localize part of settings page

This commit is contained in:
mrjvs 2023-11-26 16:33:04 +01:00
parent 0ef492f58b
commit 5b71aae159
10 changed files with 195 additions and 130 deletions

View File

@ -27,6 +27,10 @@
},
"migration": {
"failed": "Failed to migrate your data."
},
"dmca": {
"title": "",
"text": ""
}
},
"navigation": {
@ -44,6 +48,79 @@
"actions": {
"copy": "Copy"
},
"settings": {
"unsaved": "You have unsaved changes",
"reset": "Reset",
"save": "Save",
"sidebar": {
"info": {
"title": "App information",
"hostname": "Hostname",
"backendUrl": "Backend URL",
"userId": "User ID",
"notLoggedIn": "Not logged in",
"appVersion": "App version",
"backendVersion": "Backend version",
"unknownVersion": "Unknown",
"secure": "Secure",
"insecure": "Insecure"
}
},
"appearance": {
"title": "Appearance",
"activeTheme": "Active",
"themes": {
"default": "Default",
"blue": "Blue",
"teal": "Teal",
"red": "Red",
"gray": "Gray"
}
},
"account": {
"title": "Account",
"register": {
"title": "Sync to the cloud",
"text": "Instantly share your watch progress between devices and keep them synced.",
"cta": "Get started"
},
"profile": {
"title": "Edit profile picture",
"firstColor": "First color",
"secondColor": "Second color",
"userIcon": "User icon",
"finish": "Finish editing"
},
"devices": {
"title": "Devices",
"failed": "Failed to load sessions",
"deviceNameLabel": "Device name",
"removeDevice": "Remove"
}
},
"locale": {
"title": "Locale",
"language": "Application language",
"languageDescription": "Language applied to the entire application."
},
"captions": {
"title": "Captions"
},
"connections": {
"title": "Connections"
}
},
"faq": {
"title": "About us",
"q1": {
"title": "1",
"body": "Body of 1"
},
"how": {
"title": "1",
"body": "Body of 1"
}
},
"footer": {
"tagline": "Watch your favorite shows and movies with this open source streaming app.",
"links": {

View File

@ -1,4 +1,4 @@
/* eslint-disable react/no-unescaped-entities */
import { useTranslation } from "react-i18next";
import { ThinContainer } from "@/components/layout/ThinContainer";
import { Ol } from "@/components/utils/Ol";
@ -16,95 +16,18 @@ function Question(props: { title: string; children: React.ReactNode }) {
}
export function AboutPage() {
const { t } = useTranslation();
return (
<SubPageLayout>
<ThinContainer>
<Heading1>About us</Heading1>
<Heading1>{t("faq.title")}</Heading1>
<Ol
items={[
<Question title="What is Blue?">
Blue, oh so blue, like the tranquil sky on a summer's day. It's
the color of calm and serenity, a gentle embrace for your senses.
When you think of blue, you think of the vast ocean stretching
endlessly, inviting you to dive deep into its azure depths.
</Question>,
<Question title="Huh?">
Blue is the color of dreams, where the world slows down, and you
can hear the whispers of the wind in the tall grass. It's a
symphony of peacefulness that resonates with your soul, like a
melody that lingers in your heart.
</Question>,
<Question title="What the hell are you talking about?">
Blue, like, it's totally, um, the essence of like, everything, you
know? It's like, you look at it, and it's like, it's there, but
it's also not there, and it's like, you're trying to grasp the
concept of blue, but it's like trying to catch a dream in a net
made of spaghetti, you know? It's like, it's the ultimate paradox,
and it's like, it's just blowing your mind, man, like, it's like
trying to find the meaning of life in a jar of peanut butter, but
the peanut butter is made of pure energy, man, and it's like,
whoa.
</Question>,
<Question title={t("faq.q1.title")}>{t("faq.q1.body")}</Question>,
]}
/>
<Paragraph>
Blue, oh so blue, like the tranquil sky on a summer's day. It's the
color of calm and serenity, a gentle embrace for your senses. When you
think of blue, you think of the vast ocean stretching endlessly,
inviting you to dive deep into its azure depths. Blue is the color of
dreams, where the world slows down, and you can hear the whispers of
the wind in the tall grass. It's a symphony of peacefulness that
resonates with your soul, like a melody that lingers in your heart.
</Paragraph>
<Heading2>How does it work?</Heading2>
<Paragraph>
Blue, well, it's like this cosmic wavelength, man, and it's like the
universe is just vibin', you know? It's like, when you stare at the
blue, it's like you're staring at the secrets of the cosmos, like,
whoa, it's like a trippy trip to another dimension where time doesn't
even matter, and you're just floating in a sea of, like, blue, man.
And it's like, it's not just a color, it's a whole experience, like,
you're in this cosmic rollercoaster ride through the quantum soup of
existence, and you're just riding the blue wave, man.
</Paragraph>
<Paragraph>
Blue, like, it's totally, um, the essence of like, everything, you
know? It's like, you look at it, and it's like, it's there, but it's
also not there, and it's like, you're trying to grasp the concept of
blue, but it's like trying to catch a dream in a net made of
spaghetti, you know? It's like, it's the ultimate paradox, and it's
like, it's just blowing your mind, man, like, it's like trying to find
the meaning of life in a jar of peanut butter, but the peanut butter
is made of pure energy, man, and it's like, whoa.
</Paragraph>
<Heading2>Frequently asked questions</Heading2>
<Paragraph>
Blue, blue, b-b-b-bluuuuuueeeeeeeee, zippity zappity zoooooo, it's
like, you know, it's like, blue is like, um, you know, it's like, um,
like a thing, but it's also not a thing, and it's like, whoa, dude,
it's like, it's like trying to juggle invisible watermelons while
riding a unicycle made of rubber bands and ketchup, and it's like,
you're just floating in the cosmic jellyfish of existence, and the
jellyfish are like, playing the accordion, man, and it's like, the
accordion is made of, like, spaghetti and, like, um, interdimensional
cheese, and it's like, whoa, dude, like, whoa.
</Paragraph>
<Paragraph>
Bloo-bloo-bloo, bleepity-bloop, blibber-blabber, blarble-blurble, blue
is like, um, you know, flibberflabberfloober, like,
zoomity-zamity-zoom, and it's like, um, sproingity-sproing, like, uh,
gibber-gabber-gobblygook, you know, it's like, um,
jibber-jabber-jibberish, like, whatchamacallit, thingamajig,
doodad-doodad-dingdong, like, ploopity-ploop, um, blibbity-blam,
flibbity-floo, like, gobbledygook-gobbledygook,
whoopsy-daisy-dingleberry, and it's like, uh,
flibberflabberflooberzoomity-sproing, um, like,
blibber-gibber-jibber-jabber, thingamajig-whatchamacallit, like, you
know, thingamajig-doodad-doodledee, and it's like, um,
doodad-gobbledygook-doodley-doo, like,
ploopity-whoopsy-doodleberry-flibber, you know, it's like, uh, blue,
man, like, totally, um, blue.
</Paragraph>
<Heading2>{t("faq.how.title")}</Heading2>
<Paragraph>{t("faq.how.body")}</Paragraph>
</ThinContainer>
</SubPageLayout>
);

View File

@ -7,14 +7,15 @@ import { Heading1, Paragraph } from "@/components/utils/Text";
import { SubPageLayout } from "./layouts/SubPageLayout";
// TODO make email a constant
export function DmcaPage() {
const { t } = useTranslation();
return (
<SubPageLayout>
<ThinContainer>
<Heading1>{t("dmca.title")}</Heading1>
<Paragraph>{t("dmca.description")}</Paragraph>
<Heading1>{t("screens.dmca.title")}</Heading1>
<Paragraph>{t("screens.dmca.text")}</Paragraph>
<Paragraph className="flex space-x-3 items-center">
<Icon icon={Icons.MAIL} />
<span>dmca@movie-web.app</span>

View File

@ -1,5 +1,6 @@
import classNames from "classnames";
import { useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use";
import {
@ -96,6 +97,7 @@ export function AccountSettings(props: {
}
export function SettingsPage() {
const { t } = useTranslation();
const activeTheme = useThemeStore((s) => s.theme);
const setTheme = useThemeStore((s) => s.setTheme);
@ -244,21 +246,21 @@ export function SettingsPage() {
state.changed ? "opacity-100" : "opacity-0"
}`}
>
<p className="text-type-danger">You have unsaved changes</p>
<p className="text-type-danger">{t("settings.unsaved")}</p>
<div className="space-x-3 w-full md:w-auto flex">
<Button
className="w-full md:w-auto"
theme="secondary"
onClick={state.reset}
>
Reset
{t("settings.reset")}
</Button>
<Button
className="w-full md:w-auto"
theme="purple"
onClick={saveChanges}
>
Save
{t("settings.save")}
</Button>
</div>
</div>

View File

@ -1,4 +1,5 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use";
import { SessionResponse } from "@/backend/accounts/auth";
@ -18,6 +19,7 @@ export function Device(props: {
isCurrent?: boolean;
onRemove?: () => void;
}) {
const { t } = useTranslation();
const url = useBackendUrl();
const token = useAuthStore((s) => s.account?.token);
const [result, exec] = useAsyncFn(async () => {
@ -32,12 +34,14 @@ export function Device(props: {
paddingClass="px-6 py-4"
>
<div className="font-medium">
<SecondaryLabel>Device name</SecondaryLabel>
<SecondaryLabel>
{t("settings.account.devices.deviceNameLabel")}
</SecondaryLabel>
<p className="text-white">{props.name}</p>
</div>
{!props.isCurrent ? (
<Button theme="danger" loading={result.loading} onClick={exec}>
Remove
{t("settings.account.devices.removeDevice")}
</Button>
) : null}
</SettingsCard>
@ -50,6 +54,7 @@ export function DeviceListPart(props: {
sessions: SessionResponse[];
onChange?: () => void;
}) {
const { t } = useTranslation();
const seed = useAuthStore((s) => s.account?.seed);
const sessions = props.sessions;
const currentSessionId = useAuthStore((s) => s.account?.sessionId);
@ -75,10 +80,10 @@ export function DeviceListPart(props: {
return (
<div>
<Heading2 border className="mt-0 mb-9">
Devices
{t("settings.account.devices.title")}
</Heading2>
{props.error ? (
<p>Failed to load sessions</p>
<p>{t("settings.account.devices.failed")}</p>
) : props.loading ? (
<Loading />
) : (

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown";
import { Heading1 } from "@/components/utils/Text";
@ -8,7 +10,8 @@ export function LocalePart(props: {
language: string;
setLanguage: (l: string) => void;
}) {
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.code));
const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
@ -18,14 +21,16 @@ export function LocalePart(props: {
leftIcon: <FlagIcon countryCode={opt.code} />,
}));
const selected = options.find((t) => t.id === props.language);
const selected = options.find((item) => item.id === props.language);
return (
<div>
<Heading1 border>Locale</Heading1>
<p className="text-white font-bold mb-3">Application language</p>
<Heading1 border>{t("settings.locale.title")}</Heading1>
<p className="text-white font-bold mb-3">
{t("settings.locale.language")}
</p>
<p className="max-w-[20rem] font-medium">
Language applied to the entire application.
{t("settings.locale.languageDescription")}
</p>
<Dropdown
options={options}

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { ColorPicker } from "@/components/form/ColorPicker";
import { IconPicker } from "@/components/form/IconPicker";
@ -17,30 +19,34 @@ export interface ProfileEditModalProps {
}
export function ProfileEditModal(props: ProfileEditModalProps) {
const { t } = useTranslation();
return (
<Modal id={props.id}>
<ModalCard>
<Heading2 className="!mt-0">Edit profile picture</Heading2>
<Heading2 className="!mt-0">
{t("settings.account.profile.title")}
</Heading2>
<div className="space-y-6">
<ColorPicker
label="First color"
label={t("settings.account.profile.firstColor")}
value={props.colorA}
onInput={props.setColorA}
/>
<ColorPicker
label="Second color"
label={t("settings.account.profile.secondColor")}
value={props.colorB}
onInput={props.setColorB}
/>
<IconPicker
label="User icon"
label={t("settings.account.profile.userIcon")}
value={props.userIcon}
onInput={props.setUserIcon}
/>
</div>
<div className="flex justify-center mt-8">
<Button theme="purple" className="!px-20" onClick={props.close}>
Finish editing
{t("settings.account.profile.finish")}
</Button>
</div>
</ModalCard>

View File

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { Button } from "@/components/buttons/Button";
@ -6,6 +7,7 @@ import { Heading3 } from "@/components/utils/Text";
export function RegisterCalloutPart() {
const history = useHistory();
const { t } = useTranslation();
return (
<div>
@ -14,15 +16,14 @@ export function RegisterCalloutPart() {
className="grid grid-cols-2 gap-12 mt-5"
>
<div>
<Heading3>Sync to the cloud</Heading3>
<Heading3>{t("settings.account.register.title")}</Heading3>
<p className="text-type-text">
Instantly share your watch progress between devices and keep them
synced.
{t("settings.account.register.text")}
</p>
</div>
<div className="flex justify-end items-center">
<Button theme="purple" onClick={() => history.push("/register")}>
Get started
{t("settings.account.register.cta")}
</Button>
</div>
</SolidSettingsCard>

View File

@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Sticky from "react-sticky-el";
import { useAsync } from "react-use";
@ -14,16 +15,22 @@ import { useAuthStore } from "@/stores/auth";
const rem = 16;
function SecureBadge(props: { url: string }) {
const { t } = useTranslation();
const secure = props.url.startsWith("https://");
return (
<div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold">
<Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} />
Secure
{t(
secure
? "settings.sidebar.info.secure"
: "settings.sidebar.info.insecure"
)}
</div>
);
}
export function SidebarPart() {
const { t } = useTranslation();
const { isMobile } = useIsMobile();
const { account } = useAuthStore();
// eslint-disable-next-line no-restricted-globals
@ -31,11 +38,31 @@ export function SidebarPart() {
const [activeLink, setActiveLink] = useState("");
const settingLinks = [
{ text: "Account", id: "settings-account", icon: Icons.USER },
{ text: "Locale", id: "settings-locale", icon: Icons.BOOKMARK },
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB },
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
{ text: "Connections", id: "settings-connection", icon: Icons.LINK },
{
textKey: "settings.account.title",
id: "settings-account",
icon: Icons.USER,
},
{
textKey: "settings.locale.title",
id: "settings-locale",
icon: Icons.BOOKMARK,
},
{
textKey: "settings.appearance.title",
id: "settings-appearance",
icon: Icons.GITHUB,
},
{
textKey: "settings.captions.title",
id: "settings-captions",
icon: Icons.CAPTIONS,
},
{
textKey: "settings.connections.title",
id: "settings-connection",
icon: Icons.LINK,
},
];
const backendUrl = useBackendUrl();
@ -103,24 +130,29 @@ export function SidebarPart() {
onClick={() => scrollTo(v.id)}
key={v.id}
>
{v.text}
{t(v.textKey)}
</SidebarLink>
))}
</SidebarSection>
<Divider />
</div>
<SidebarSection className="text-sm" title="App information">
<SidebarSection
className="text-sm"
title={t("settings.sidebar.info.title")}
>
<div className="px-3 py-3.5 rounded-lg bg-largeCard-background bg-opacity-50 grid grid-cols-2 gap-4">
{/* Hostname */}
<div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium">Hostname</p>
<p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.hostname")}
</p>
<p className="text-white">{hostname}</p>
</div>
{/* Backend URL */}
<div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium flex items-center">
Backend URL
{t("settings.sidebar.info.backendUrl")}
<SecureBadge url={backendUrl} />
</p>
<p className="text-white">
@ -130,13 +162,19 @@ export function SidebarPart() {
{/* User ID */}
<div className="col-span-2 space-y-1">
<p className="text-type-dimmed font-medium">User ID</p>
<p className="text-white">{account?.userId ?? "Not logged in"}</p>
<p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.userId")}
</p>
<p className="text-white">
{account?.userId ?? t("settings.sidebar.info.notLoggedIn")}
</p>
</div>
{/* App version */}
<div className="col-span-1 space-y-1">
<p className="text-type-dimmed font-medium">App version</p>
<p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.appVersion")}
</p>
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-block">
{conf().APP_VERSION}
</p>
@ -144,7 +182,9 @@ export function SidebarPart() {
{/* Backend version */}
<div className="col-span-1 space-y-1">
<p className="text-type-dimmed font-medium">Backend version</p>
<p className="text-type-dimmed font-medium">
{t("settings.sidebar.info.backendVersion")}
</p>
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-flex items-center gap-1">
{backendMeta.error ? (
<Icon
@ -155,7 +195,8 @@ export function SidebarPart() {
{backendMeta.loading ? (
<div className="h-4 w-12 bg-type-dimmed/20 rounded" />
) : (
backendMeta?.value?.version || "Unknown"
backendMeta?.value?.version ||
t("settings.sidebar.info.unknownVersion")
)}
</p>
</div>

View File

@ -1,4 +1,5 @@
import classNames from "classnames";
import { useTranslation } from "react-i18next";
import { Icon, Icons } from "@/components/Icon";
import { Heading1 } from "@/components/utils/Text";
@ -6,19 +7,19 @@ import { Heading1 } from "@/components/utils/Text";
const availableThemes = [
{
id: "blue",
name: "Blue",
key: "settings.themes.blue",
},
{
id: "teal",
name: "Teal",
key: "settings.themes.teal",
},
{
id: "red",
name: "Red",
key: "settings.themes.red",
},
{
id: "gray",
name: "Gray",
key: "settings.themes.gray",
},
];
@ -28,6 +29,8 @@ function ThemePreview(props: {
name: string;
onClick?: () => void;
}) {
const { t } = useTranslation();
return (
<div
className={classNames(props.selector, "cursor-pointer group tabbable")}
@ -58,7 +61,6 @@ function ThemePreview(props: {
)}
/>
{/* Mini movie-web. So Kawaiiiii! */}
{/* ^ can we keep this comment in forever please? - Jip */}
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-3/5 h-4/5 rounded-t-lg -mb-px bg-background-main overflow-hidden">
<div className="relative w-full h-full">
{/* Background color */}
@ -106,7 +108,7 @@ function ThemePreview(props: {
props.active ? "opacity-100" : "opacity-0 pointer-events-none"
)}
>
Active
{t("settings.appearance.activeTheme")}
</span>
</div>
</div>
@ -117,13 +119,15 @@ export function ThemePart(props: {
active: string | null;
setTheme: (theme: string | null) => void;
}) {
const { t } = useTranslation();
return (
<div>
<Heading1 border>Appearance</Heading1>
<Heading1 border>{t("settings.appearance.title")}</Heading1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
{/* default theme */}
<ThemePreview
name="Default"
name={t("settings.appearance.themes.default")}
selector="theme-default"
active={props.active === null}
onClick={() => props.setTheme(null)}
@ -132,7 +136,7 @@ export function ThemePart(props: {
<ThemePreview
selector={`theme-${v.id}`}
active={props.active === v.id}
name={v.name}
name={t(v.key)}
key={v.id}
onClick={() => props.setTheme(v.id)}
/>