mirror of
https://github.com/sussy-code/smov.git
synced 2025-01-17 01:51:24 +01:00
i18nify views
This commit is contained in:
parent
95f17b507b
commit
03ffea333a
9 changed files with 97 additions and 41 deletions
41
public/locales/en-GB/translation.json
Normal file
41
public/locales/en-GB/translation.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"name": "movie-web"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"loading": "Fetching your favourite shows...",
|
||||||
|
"providersFailed": "{{fails}}/{{total}} providers failed!",
|
||||||
|
"allResults": "That's all we have!",
|
||||||
|
"noResults": "We couldn't find anything!",
|
||||||
|
"allFailed": "All providers have failed!",
|
||||||
|
"headingTitle": "Search results",
|
||||||
|
"headingLink": "Back to home",
|
||||||
|
"bookmarks": "Bookmarks",
|
||||||
|
"continueWatching": "Continue Watching",
|
||||||
|
"tagline": "Because watching legally is boring",
|
||||||
|
"title": "What do you want to watch?",
|
||||||
|
"placeholder": "What do you want to watch?"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"invalidUrl": "Your URL may be invalid",
|
||||||
|
"arrowText": "Go back"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"backArrow": "Back to home",
|
||||||
|
"media": {
|
||||||
|
"title": "Couldn't find that media",
|
||||||
|
"description": "We couldn't find the media you requested. Either it's been removed or you tampered with the URL"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"title": "This provider has been disabled",
|
||||||
|
"description": "We had issues with the provider or it was too unstable to use, so we had to disable it."
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Couldn't find that page",
|
||||||
|
"description": "We looked everywhere: under the bins, in the closet, behind the proxy but ultimately couldn't find the page you are looking for."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errorBoundary": {
|
||||||
|
"text": "The app encountered an error and wasn't able to recover, please report it to the"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"global": {
|
|
||||||
"name": "movie-web"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function BrandPill(props: { clickable?: boolean }) {
|
export function BrandPill(props: { clickable?: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center space-x-2 rounded-full bg-bink-100 bg-opacity-50 px-4 py-2 text-bink-600 ${
|
className={`flex items-center space-x-2 rounded-full bg-bink-100 bg-opacity-50 px-4 py-2 text-bink-600 ${props.clickable
|
||||||
props.clickable
|
|
||||||
? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95"
|
? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="text-xl" icon={Icons.MOVIE_WEB} />
|
<Icon className="text-xl" icon={Icons.MOVIE_WEB} />
|
||||||
<span className="font-semibold text-white">movie-web</span>
|
<span className="font-semibold text-white">{t('global.name')}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,7 @@ i18n
|
||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en-GB',
|
||||||
debug: true,
|
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { HashRouter } from "react-router-dom";
|
import { HashRouter } from "react-router-dom";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
@ -10,7 +10,9 @@ ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
<Suspense fallback="">
|
||||||
<App />
|
<App />
|
||||||
|
</Suspense>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
|
|
|
@ -100,8 +100,7 @@ export const xemovieScraper: MWMediaProvider = {
|
||||||
const data = JSON.parse(
|
const data = JSON.parse(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
eval(
|
eval(
|
||||||
`(${
|
`(${script.textContent.replace("const data = ", "").split("};")[0]
|
||||||
script.textContent.replace("const data = ", "").split("};")[0]
|
|
||||||
}})`
|
}})`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
useBookmarkContext,
|
useBookmarkContext,
|
||||||
} from "@/state/bookmark";
|
} from "@/state/bookmark";
|
||||||
import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
|
import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { NotFoundChecks } from "./notfound/NotFoundChecks";
|
import { NotFoundChecks } from "./notfound/NotFoundChecks";
|
||||||
|
|
||||||
interface StyledMediaViewProps {
|
interface StyledMediaViewProps {
|
||||||
|
@ -105,6 +106,8 @@ function StyledMediaFooter(props: StyledMediaFooterProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingMediaFooter(props: { error?: boolean }) {
|
function LoadingMediaFooter(props: { error?: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="mt-5">
|
<Paper className="mt-5">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
@ -117,7 +120,7 @@ function LoadingMediaFooter(props: { error?: boolean }) {
|
||||||
{props.error ? (
|
{props.error ? (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
||||||
<p>Your url may be invalid</p>
|
<p>{t('media.invalidUrl')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LoadingSeasons />
|
<LoadingSeasons />
|
||||||
|
@ -183,6 +186,7 @@ function MediaViewContent(props: { portable: MWPortableMedia }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaView() {
|
export function MediaView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const mediaPortable: MWPortableMedia | undefined = usePortableMedia();
|
const mediaPortable: MWPortableMedia | undefined = usePortableMedia();
|
||||||
const reactHistory = useHistory();
|
const reactHistory = useHistory();
|
||||||
|
|
||||||
|
@ -196,7 +200,7 @@ export function MediaView() {
|
||||||
: reactHistory.push("/")
|
: reactHistory.push("/")
|
||||||
}
|
}
|
||||||
direction="left"
|
direction="left"
|
||||||
linkText="Go back"
|
linkText={t('media.arrowText')}
|
||||||
/>
|
/>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
<NotFoundChecks portable={mediaPortable}>
|
<NotFoundChecks portable={mediaPortable}>
|
||||||
|
|
|
@ -18,9 +18,11 @@ import {
|
||||||
getIfBookmarkedFromPortable,
|
getIfBookmarkedFromPortable,
|
||||||
useBookmarkContext,
|
useBookmarkContext,
|
||||||
} from "@/state/bookmark/context";
|
} from "@/state/bookmark/context";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
function SearchLoading() {
|
function SearchLoading() {
|
||||||
return <Loading className="my-24" text="Fetching your favourite shows..." />;
|
const { t } = useTranslation();
|
||||||
|
return <Loading className="my-24" text={t('search.loading') || "Fetching your favourite shows..."} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchSuffix(props: {
|
function SearchSuffix(props: {
|
||||||
|
@ -28,6 +30,8 @@ function SearchSuffix(props: {
|
||||||
total: number;
|
total: number;
|
||||||
resultsSize: number;
|
resultsSize: number;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const allFailed: boolean = props.fails === props.total;
|
const allFailed: boolean = props.fails === props.total;
|
||||||
const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH;
|
const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH;
|
||||||
|
|
||||||
|
@ -43,13 +47,13 @@ function SearchSuffix(props: {
|
||||||
<div>
|
<div>
|
||||||
{props.fails > 0 ? (
|
{props.fails > 0 ? (
|
||||||
<p className="text-red-400">
|
<p className="text-red-400">
|
||||||
{props.fails}/{props.total} providers failed!
|
{t('search.providersFailed', { fails: props.fails, total: props.total })}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{props.resultsSize > 0 ? (
|
{props.resultsSize > 0 ? (
|
||||||
<p>That's all we have!</p>
|
<p>{t('search.allResults')}</p>
|
||||||
) : (
|
) : (
|
||||||
<p>We couldn't find anything!</p>
|
<p>{t('search.noResults')}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -57,7 +61,7 @@ function SearchSuffix(props: {
|
||||||
{/* Error result */}
|
{/* Error result */}
|
||||||
{allFailed ? (
|
{allFailed ? (
|
||||||
<div>
|
<div>
|
||||||
<p>All providers have failed!</p>
|
<p>{t('search.allFailed')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,6 +75,8 @@ function SearchResultsView({
|
||||||
searchQuery: MWQuery;
|
searchQuery: MWQuery;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [results, setResults] = useState<MWMassProviderOutput | undefined>();
|
const [results, setResults] = useState<MWMassProviderOutput | undefined>();
|
||||||
const [runSearchQuery, loading, error, success] = useLoading(
|
const [runSearchQuery, loading, error, success] = useLoading(
|
||||||
(query: MWQuery) => SearchProviders(query)
|
(query: MWQuery) => SearchProviders(query)
|
||||||
|
@ -91,9 +97,9 @@ function SearchResultsView({
|
||||||
{/* results */}
|
{/* results */}
|
||||||
{success && results?.results.length ? (
|
{success && results?.results.length ? (
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
title="Search results"
|
title={t('search.headingTitle') || "Search results"}
|
||||||
icon={Icons.SEARCH}
|
icon={Icons.SEARCH}
|
||||||
linkText="Back to home"
|
linkText={t('search.headingLink') || "Back to home"}
|
||||||
onClick={() => clear()}
|
onClick={() => clear()}
|
||||||
>
|
>
|
||||||
{results.results.map((v) => (
|
{results.results.map((v) => (
|
||||||
|
@ -124,6 +130,8 @@ function SearchResultsView({
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExtraItems() {
|
function ExtraItems() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { getFilteredBookmarks } = useBookmarkContext();
|
const { getFilteredBookmarks } = useBookmarkContext();
|
||||||
const { getFilteredWatched } = useWatchedContext();
|
const { getFilteredWatched } = useWatchedContext();
|
||||||
|
|
||||||
|
@ -138,7 +146,7 @@ function ExtraItems() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-16 mt-32">
|
<div className="mb-16 mt-32">
|
||||||
{bookmarks.length > 0 ? (
|
{bookmarks.length > 0 ? (
|
||||||
<SectionHeading title="Bookmarks" icon={Icons.BOOKMARK}>
|
<SectionHeading title={t('search.bookmarks') || "Bookmarks"} icon={Icons.BOOKMARK}>
|
||||||
{bookmarks.map((v) => (
|
{bookmarks.map((v) => (
|
||||||
<WatchedMediaCard
|
<WatchedMediaCard
|
||||||
key={[v.mediaId, v.providerId].join("|")}
|
key={[v.mediaId, v.providerId].join("|")}
|
||||||
|
@ -148,7 +156,7 @@ function ExtraItems() {
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
) : null}
|
) : null}
|
||||||
{watchedItems.length > 0 ? (
|
{watchedItems.length > 0 ? (
|
||||||
<SectionHeading title="Continue Watching" icon={Icons.CLOCK}>
|
<SectionHeading title={t('search.continueWatching') || "Continue Watching"} icon={Icons.CLOCK}>
|
||||||
{watchedItems.map((v) => (
|
{watchedItems.map((v) => (
|
||||||
<WatchedMediaCard
|
<WatchedMediaCard
|
||||||
key={[v.mediaId, v.providerId].join("|")}
|
key={[v.mediaId, v.providerId].join("|")}
|
||||||
|
@ -163,6 +171,8 @@ function ExtraItems() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchView() {
|
export function SearchView() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [searching, setSearching] = useState<boolean>(false);
|
const [searching, setSearching] = useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [search, setSearch, setSearchUnFocus] = useSearchQuery();
|
const [search, setSearch, setSearchUnFocus] = useSearchQuery();
|
||||||
|
@ -195,14 +205,14 @@ export function SearchView() {
|
||||||
{/* input section */}
|
{/* input section */}
|
||||||
<div className="mt-44 space-y-16 text-center">
|
<div className="mt-44 space-y-16 text-center">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Tagline>Because watching legally is boring</Tagline>
|
<Tagline>{t('search.tagline')}</Tagline>
|
||||||
<Title>What movie do you want to watch?</Title>
|
<Title>{t('search.title')}</Title>
|
||||||
</div>
|
</div>
|
||||||
<SearchBarInput
|
<SearchBarInput
|
||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
value={search}
|
value={search}
|
||||||
onUnFocus={setSearchUnFocus}
|
onUnFocus={setSearchUnFocus}
|
||||||
placeholder="What movie do you want to watch?"
|
placeholder={t('search.placeholder') || "What do you want to watch?"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Icons } from "@/components/Icon";
|
||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
import { ArrowLink } from "@/components/text/ArrowLink";
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
function NotFoundWrapper(props: { children?: ReactNode }) {
|
function NotFoundWrapper(props: { children?: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
@ -17,52 +18,55 @@ function NotFoundWrapper(props: { children?: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotFoundMedia() {
|
export function NotFoundMedia() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
||||||
<IconPatch
|
<IconPatch
|
||||||
icon={Icons.EYE_SLASH}
|
icon={Icons.EYE_SLASH}
|
||||||
className="mb-6 text-xl text-bink-600"
|
className="mb-6 text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<Title>Couldn't find that media</Title>
|
<Title>{t('notFound.media.title')}</Title>
|
||||||
<p className="mt-5 mb-12 max-w-sm">
|
<p className="mt-5 mb-12 max-w-sm">
|
||||||
We couldn't find the media you requested. Either it's been
|
{t('notFound.media.description')}
|
||||||
removed or you tampered with the URL
|
|
||||||
</p>
|
</p>
|
||||||
<ArrowLink to="/" linkText="Back to home" />
|
<ArrowLink to="/" linkText={t('notFound.backArrow')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotFoundProvider() {
|
export function NotFoundProvider() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center p-5 text-center">
|
||||||
<IconPatch
|
<IconPatch
|
||||||
icon={Icons.EYE_SLASH}
|
icon={Icons.EYE_SLASH}
|
||||||
className="mb-6 text-xl text-bink-600"
|
className="mb-6 text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<Title>This provider has been disabled</Title>
|
<Title>{t('notFound.provider.title')}</Title>
|
||||||
<p className="mt-5 mb-12 max-w-sm">
|
<p className="mt-5 mb-12 max-w-sm">
|
||||||
We had issues with the provider or it was too unstable to use, so we had
|
{t('notFound.provider.description')}
|
||||||
to disable it.
|
|
||||||
</p>
|
</p>
|
||||||
<ArrowLink to="/" linkText="Back to home" />
|
<ArrowLink to="/" linkText={t('notFound.backArrow')} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotFoundPage() {
|
export function NotFoundPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotFoundWrapper>
|
<NotFoundWrapper>
|
||||||
<IconPatch
|
<IconPatch
|
||||||
icon={Icons.EYE_SLASH}
|
icon={Icons.EYE_SLASH}
|
||||||
className="mb-6 text-xl text-bink-600"
|
className="mb-6 text-xl text-bink-600"
|
||||||
/>
|
/>
|
||||||
<Title>Couldn't find that page</Title>
|
<Title>{t('notFound.page.title')}</Title>
|
||||||
<p className="mt-5 mb-12 max-w-sm">
|
<p className="mt-5 mb-12 max-w-sm">
|
||||||
We looked everywhere: under the bins, in the closet, behind the proxy
|
{t('notFound.page.description')}
|
||||||
but ultimately couldn't find the page you are looking for.
|
|
||||||
</p>
|
</p>
|
||||||
<ArrowLink to="/" linkText="Back to home" />
|
<ArrowLink to="/" linkText={t('notFound.backArrow')} />
|
||||||
</NotFoundWrapper>
|
</NotFoundWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue