Browse Source

localize part of settings page

pull/497/head
mrjvs 2 years ago
parent
commit
5b71aae159
  1. 77
      src/assets/locales/en.json
  2. 89
      src/pages/About.tsx
  3. 5
      src/pages/Dmca.tsx
  4. 8
      src/pages/Settings.tsx
  5. 13
      src/pages/parts/settings/DeviceListPart.tsx
  6. 15
      src/pages/parts/settings/LocalePart.tsx
  7. 16
      src/pages/parts/settings/ProfileEditModal.tsx
  8. 9
      src/pages/parts/settings/RegisterCalloutPart.tsx
  9. 71
      src/pages/parts/settings/SidebarPart.tsx
  10. 22
      src/pages/parts/settings/ThemePart.tsx

77
src/assets/locales/en.json

@ -27,6 +27,10 @@ @@ -27,6 +27,10 @@
},
"migration": {
"failed": "Failed to migrate your data."
},
"dmca": {
"title": "",
"text": ""
}
},
"navigation": {
@ -44,6 +48,79 @@ @@ -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": {

89
src/pages/About.tsx

@ -1,4 +1,4 @@ @@ -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 }) { @@ -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>
);

5
src/pages/Dmca.tsx

@ -7,14 +7,15 @@ import { Heading1, Paragraph } from "@/components/utils/Text"; @@ -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>

8
src/pages/Settings.tsx

@ -1,5 +1,6 @@ @@ -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: { @@ -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() { @@ -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>

13
src/pages/parts/settings/DeviceListPart.tsx

@ -1,4 +1,5 @@ @@ -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: { @@ -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: { @@ -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: { @@ -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: { @@ -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 />
) : (

15
src/pages/parts/settings/LocalePart.tsx

@ -1,3 +1,5 @@ @@ -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: { @@ -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: { @@ -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}

16
src/pages/parts/settings/ProfileEditModal.tsx

@ -1,3 +1,5 @@ @@ -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 { @@ -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>

9
src/pages/parts/settings/RegisterCalloutPart.tsx

@ -1,3 +1,4 @@ @@ -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"; @@ -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() { @@ -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>

71
src/pages/parts/settings/SidebarPart.tsx

@ -1,4 +1,5 @@ @@ -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"; @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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>

22
src/pages/parts/settings/ThemePart.tsx

@ -1,4 +1,5 @@ @@ -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"; @@ -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: { @@ -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: { @@ -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: { @@ -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: { @@ -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: { @@ -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)}
/>

Loading…
Cancel
Save