From a4808415db97335bb52b41eafde6b1d232f8b820 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 28 Nov 2023 21:11:46 +0100 Subject: [PATCH] Localize the rest of everything --- src/assets/locales/en.json | 105 +++++++++++++++++- src/components/player/atoms/EpisodeTitle.tsx | 2 +- src/components/player/atoms/Episodes.tsx | 32 ++++-- .../player/atoms/NextEpisodeButton.tsx | 6 +- src/components/player/atoms/Skips.tsx | 4 +- src/components/player/atoms/Time.tsx | 71 ++++++------ .../atoms/settings/CaptionSettingsView.tsx | 14 ++- .../player/atoms/settings/CaptionsView.tsx | 17 ++- .../player/atoms/settings/Downloads.tsx | 71 ++++++------ .../atoms/settings/PlaybackSettingsView.tsx | 8 +- .../player/atoms/settings/QualityView.tsx | 16 +-- .../player/atoms/settings/SettingsMenu.tsx | 27 +++-- .../atoms/settings/SourceSelectingView.tsx | 29 +++-- src/components/player/base/BackLink.tsx | 4 +- src/components/player/display/base.ts | 2 +- .../player/display/displayInterface.ts | 3 +- .../player/internals/ContextMenu/Items.tsx | 1 - .../player/internals/ContextMenu/Misc.tsx | 2 +- .../player/internals/HeadUpdater.tsx | 2 +- .../player/internals/KeyboardEvents.tsx | 7 +- .../player/internals/MetaReporter.tsx | 4 + .../player/internals/ScrapeCard.tsx | 10 +- src/components/player/utils/handleBuffered.ts | 1 + .../player/utils/mediaErrorDetails.ts | 25 ++--- src/pages/parts/errors/ErrorCard.tsx | 8 +- 25 files changed, 313 insertions(+), 158 deletions(-) delete mode 100644 src/components/player/internals/ContextMenu/Items.tsx diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 8aabe71b..c8be940e 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -72,13 +72,25 @@ "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" + }, + "items": { + "pending": "Checking for videos...", + "notFound": "Doesn't have the video", + "failure": "Error occured" } }, "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" + "homeButton": "Go home", + "errors": { + "errorAborted": "The fetching of the associated resource was aborted by the user's request.", + "errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", + "errorDecode": "Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.", + "errorNotSupported": "The associated resource or media provider object has been found to be unsuitable.", + "errorGenericMedia": "Unknown media error occured" + } }, "metadata": { "notFound": { @@ -93,6 +105,97 @@ "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" } + }, + "back": { + "default": "Back to home", + "short": "Back" + }, + "time": { + "short": "-{{timeLeft}}", + "regular": "{{timeWatched}} / {{duration}}", + "remaining": "{{timeLeft}} left โ€ข Finish at {{timeFinished, datetime}}" + }, + "nextEpisode": { + "next": "Next episode", + "cancel": "Cancel" + }, + "menus": { + "settings": { + "videoSection": "Video settings", + "experienceSection": "Viewing Experience", + "enableCaptions": "Enable Captions", + "captionItem": "Caption settings", + "sourceItem": "Video sources", + "playbackItem": "Playback settings", + "downloadItem": "Download", + "qualityItem": "Quality" + }, + "episodes": { + "button": "Episodes", + "loadingTitle": "Loading...", + "loadingList": "Loading...", + "loadingError": "Error loading season", + "emptyState": "There are no episodes in this season, check back later!", + "episodeBadge": "E{{episode}}" + }, + "sources": { + "title": "Sources", + "unknownOption": "Unknown", + "noStream": { + "title": "No stream", + "text": "This source has no streams for this movie or show." + }, + "noEmbeds": { + "title": "No embeds found", + "text": "We were unable to find any embeds for this source, please try another." + }, + "failed": { + "title": "Failed to scrape", + "text": "We were unable to find any videos for this source. Don't come bitchin' to us about it, just try another source." + } + }, + "captions": { + "title": "Captions", + "customizeLabel": "Customize", + "settings": { + "fixCapitals": "Fix capitalization", + "delay": "Caption delay" + }, + "customChoice": "Upload captions", + "offChoice": "Off", + "unknownLanguage": "Unknown" + }, + "downloads": { + "title": "Download", + "disclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.", + "hlsExplanation": "Insert explanation for why you can't download HLS here", + "downloadVideo": "Download video", + "downloadCaption": "Download current caption", + "onPc": { + "title": "Downloading on PC", + "shortTitle": "Download / PC", + "1": "On PC, right click the video and select Save video as" + }, + "onAndroid": { + "title": "Downloading on Android", + "shortTitle": "Download / Android", + "1": "To download on Android, tap and hold on the video, then select save." + }, + "onIos": { + "title": "Downloading on iOS", + "shortTitle": "Download / iOS", + "1": "To download on iOS, click , then Save to Files . All that's left to do now is to pick a nice and cozy folder for your video!" + } + }, + "playback": { + "title": "Playback settings", + "speedLabel": "Playback speed" + }, + "quality": { + "title": "Quality", + "automaticLabel": "Automatic quality", + "hint": "You can try <0>switching source to get different quality options." + } } }, "home": { diff --git a/src/components/player/atoms/EpisodeTitle.tsx b/src/components/player/atoms/EpisodeTitle.tsx index f3cefcef..b36909b1 100644 --- a/src/components/player/atoms/EpisodeTitle.tsx +++ b/src/components/player/atoms/EpisodeTitle.tsx @@ -11,7 +11,7 @@ export function EpisodeTitle() { return (
- {t("seasons.seasonAndEpisode", { + {t("media.episodeDisplay", { season: meta?.season?.number, episode: meta?.episode?.number, })} diff --git a/src/components/player/atoms/Episodes.tsx b/src/components/player/atoms/Episodes.tsx index d69b9275..06439bdd 100644 --- a/src/components/player/atoms/Episodes.tsx +++ b/src/components/player/atoms/Episodes.tsx @@ -50,6 +50,7 @@ function SeasonsView({ selectedSeason: string; setSeason: (id: string) => void; }) { + const { t } = useTranslation(); const meta = usePlayerStore((s) => s.meta); const [loadingState, seasons] = useSeasonData( meta?.tmdbId ?? "", @@ -73,13 +74,19 @@ function SeasonsView({ ); } else if (loadingState.error) - content = Error loading season; + content = ( + {t("player.menus.episodes.loadingError")} + ); else if (loadingState.loading) - content = Loading...; + content = ( + {t("player.menus.episodes.loadingList")} + ); return ( - {meta?.title} + + {meta?.title ?? t("player.menus.episodes.loadingTitle")} + {content} ); @@ -120,15 +127,19 @@ function EpisodesView({ let content: ReactNode = null; if (loadingState.error) - content = Error loading season; + content = ( + {t("player.menus.episodes.loadingError")} + ); else if (loadingState.loading) - content = Loading...; + content = ( + {t("player.menus.episodes.loadingList")} + ); else if (loadingState.value) { content = ( {loadingState.value.season.episodes.length === 0 ? ( - There are no episodes in this season, check back later! + {t("player.menus.episodes.emptyState")} ) : null} {loadingState.value.season.episodes.map((ep) => { @@ -167,7 +178,9 @@ function EpisodesView({ : "bg-opacity-50" )} > - E{ep.number} + {t("player.menus.episodes.episodeBadge", { + episode: ep.number, + })} {ep.title}
@@ -182,7 +195,8 @@ function EpisodesView({ return ( - {loadingState?.value?.season.title || t("videoPlayer.loading")} + {loadingState?.value?.season.title || + t("player.menus.episodes.loadingTitle")} {content} @@ -261,7 +275,7 @@ export function Episodes() { onClick={() => router.open("/episodes")} icon={Icons.EPISODES} > - {t("videoPlayer.buttons.episodes")} + {t("player.menus.episodes.button")} ); diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx index 2b36d172..b80df3a9 100644 --- a/src/components/player/atoms/NextEpisodeButton.tsx +++ b/src/components/player/atoms/NextEpisodeButton.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { Icon, Icons } from "@/components/Icon"; import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; @@ -41,6 +42,7 @@ export function NextEpisodeButton(props: { controlsShowing: boolean; onChange?: (meta: PlayerMeta) => void; }) { + const { t } = useTranslation(); const duration = usePlayerStore((s) => s.progress.duration); const isHidden = usePlayerStore((s) => s.interface.hideNextEpisodeBtn); const meta = usePlayerStore((s) => s.meta); @@ -96,14 +98,14 @@ export function NextEpisodeButton(props: { className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText" onClick={hideNextEpisodeButton} > - Cancel + {t("player.nextEpisode.cancel")} diff --git a/src/components/player/atoms/Skips.tsx b/src/components/player/atoms/Skips.tsx index 455ee2fc..23333bd4 100644 --- a/src/components/player/atoms/Skips.tsx +++ b/src/components/player/atoms/Skips.tsx @@ -14,7 +14,7 @@ export function SkipForward(props: { iconSizeClass?: string }) { return ( @@ -31,7 +31,7 @@ export function SkipBackward(props: { iconSizeClass?: string }) { return ( diff --git a/src/components/player/atoms/Time.tsx b/src/components/player/atoms/Time.tsx index eb984b4b..c750ef62 100644 --- a/src/components/player/atoms/Time.tsx +++ b/src/components/player/atoms/Time.tsx @@ -9,10 +9,14 @@ export function Time(props: { short?: boolean }) { const timeFormat = usePlayerStore((s) => s.interface.timeFormat); const setTimeFormat = usePlayerStore((s) => s.setTimeFormat); - const { duration, time, draggingTime } = usePlayerStore((s) => s.progress); + const { + duration: timeDuration, + time, + draggingTime, + } = usePlayerStore((s) => s.progress); const { isSeeking } = usePlayerStore((s) => s.interface); const { t } = useTranslation(); - const hasHours = durationExceedsHour(duration); + const hasHours = durationExceedsHour(timeDuration); function toggleMode() { setTimeFormat( @@ -24,47 +28,36 @@ export function Time(props: { short?: boolean }) { const currentTime = Math.min( Math.max(isSeeking ? draggingTime : time, 0), - duration + timeDuration ); - const secondsRemaining = Math.abs(currentTime - duration); + const secondsRemaining = Math.abs(currentTime - timeDuration); + + const timeLeft = formatSeconds( + secondsRemaining, + durationExceedsHour(secondsRemaining) + ); + const timeWatched = formatSeconds(currentTime, hasHours); const timeFinished = new Date(Date.now() + secondsRemaining * 1e3); + const duration = formatSeconds(timeDuration, hasHours); - const formattedTimeFinished = t("videoPlayer.finishAt", { - timeFinished, - formatParams: { - timeFinished: { hour: "numeric", minute: "numeric" }, - }, - }); - - let timeString; - let timeFinishedString; - if (props.short) { - timeString = formatSeconds(currentTime, hasHours); - timeFinishedString = `-${formatSeconds( - secondsRemaining, - durationExceedsHour(secondsRemaining) - )}`; - } else { - timeString = `${formatSeconds(currentTime, hasHours)} / ${formatSeconds( - duration, - hasHours - )}`; - timeFinishedString = `${t("videoPlayer.timeLeft", { - timeLeft: formatSeconds( - secondsRemaining, - durationExceedsHour(secondsRemaining) - ), - })} โ€ข ${formattedTimeFinished}`; - } - - const child = - timeFormat === VideoPlayerTimeFormat.REGULAR ? ( - {timeString} - ) : ( - {timeFinishedString} - ); + let localizationKey = "regular"; + if (props.short) localizationKey = "short"; + else if (timeFormat === VideoPlayerTimeFormat.REMAINING) + localizationKey = "remaining"; return ( - toggleMode()}>{child} + toggleMode()}> + + {t(`player.time.${localizationKey}`, { + timeFinished, + timeWatched, + timeLeft, + duration, + formatParams: { + timeFinished: { hour: "numeric", minute: "numeric" }, + }, + })} + + ); } diff --git a/src/components/player/atoms/settings/CaptionSettingsView.tsx b/src/components/player/atoms/settings/CaptionSettingsView.tsx index 10d2792a..8ac36917 100644 --- a/src/components/player/atoms/settings/CaptionSettingsView.tsx +++ b/src/components/player/atoms/settings/CaptionSettingsView.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Toggle } from "@/components/buttons/Toggle"; import { Icon, Icons } from "@/components/Icon"; @@ -213,6 +214,7 @@ export function CaptionSetting(props: { export const colors = ["#ffffff", "#80b1fa", "#e2e535"]; export function CaptionSettingsView({ id }: { id: string }) { + const { t } = useTranslation(); const router = useOverlayRouter(id); const styling = useSubtitleStore((s) => s.styling); const overrideCasing = useSubtitleStore((s) => s.overrideCasing); @@ -228,7 +230,7 @@ export function CaptionSettingsView({ id }: { id: string }) { setDelay(v)} @@ -238,7 +240,9 @@ export function CaptionSettingsView({ id }: { id: string }) { controlButtons />
- Fix capitalization + + {t("player.menus.captions.settings.delay")} +
updateStyling({ backgroundOpacity: v / 100 })} @@ -256,7 +260,7 @@ export function CaptionSettingsView({ id }: { id: string }) { textTransformer={(s) => `${s}%`} /> `${s}%`} @@ -264,7 +268,7 @@ export function CaptionSettingsView({ id }: { id: string }) { value={styling.size * 100} />
- Color + {t("settings.captions.colorLabel")}
{colors.map((v) => ( s.caption.selected?.language); const setCaption = usePlayerStore((s) => s.setCaption); const setCustomSubs = useSubtitleStore((s) => s.setCustomSubs); @@ -55,7 +57,7 @@ function CustomCaptionOption() { selected={lang === "custom"} onClick={() => fileInput.current?.click()} > - Upload captions + {t("player.menus.captions.customChoice")} { const input = subs.map((t) => ({ ...t, - languageName: getLanguageFromIETF(t.language) ?? "Unknown", + languageName: getLanguageFromIETF(t.language) ?? unknownChoice, })); const sorted = sortLangCodes(input.map((t) => t.language)); let results = input.sort((a, b) => { @@ -102,10 +106,11 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) { } return results; - }, [subs, searchQuery]); + }, [subs, searchQuery, unknownChoice]); } export function CaptionsView({ id }: { id: string }) { + const { t } = useTranslation(); const router = useOverlayRouter(id); const lang = usePlayerStore((s) => s.caption.selected?.language); const [currentlyDownloading, setCurrentlyDownloading] = useState< @@ -155,11 +160,11 @@ export function CaptionsView({ id }: { id: string }) { onClick={() => router.navigate("/captions/settings")} className="py-1 -my-1 px-3 -mx-3 rounded tabbable" > - Customize + {t("player.menus.captions.customizeLabel")} } > - Captions + {t("player.menus.captions.title")}
@@ -167,7 +172,7 @@ export function CaptionsView({ id }: { id: string }) {
disable()} selected={!lang}> - Off + {t("player.menus.captions.offChoice")} {content} diff --git a/src/components/player/atoms/settings/Downloads.tsx b/src/components/player/atoms/settings/Downloads.tsx index bd26e515..1ecaf317 100644 --- a/src/components/player/atoms/settings/Downloads.tsx +++ b/src/components/player/atoms/settings/Downloads.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; @@ -19,8 +20,26 @@ function useDownloadLink() { return url; } +function StyleTrans(props: { k: string }) { + return ( + , + ios_share: ( + + ), + ios_files: ( + + ), + }} + /> + ); +} + export function DownloadView({ id }: { id: string }) { const router = useOverlayRouter(id); + const { t } = useTranslation(); const downloadUrl = useDownloadLink(); const selectedCaption = usePlayerStore((s) => s.caption?.selected); @@ -37,31 +56,30 @@ export function DownloadView({ id }: { id: string }) { return ( <> router.navigate("/")}> - Download + {t("player.menus.downloads.title")}
router.navigate("/download/pc")}> - Downloading on PC + {t("player.menus.downloads.onPc.title")} router.navigate("/download/ios")}> - Downloading on iOS + {t("player.menus.downloads.onIos.title")} router.navigate("/download/android")} > - Downloading on Android + {t("player.menus.downloads.onAndroid.title")} - Downloads are taken directly from the provider. movie-web does not - have control over how the downloads are provided. +
@@ -80,15 +98,16 @@ export function DownloadView({ id }: { id: string }) { export function CantDownloadView({ id }: { id: string }) { const router = useOverlayRouter(id); + const { t } = useTranslation(); return ( <> router.navigate("/")}> - Download + {t("player.menus.downloads.title")} - Insert explanation for why you can't download HLS here + @@ -97,16 +116,16 @@ export function CantDownloadView({ id }: { id: string }) { function AndroidExplanationView({ id }: { id: string }) { const router = useOverlayRouter(id); + const { t } = useTranslation(); return ( <> router.navigate("/download")}> - Download / Android + {t("player.menus.downloads.onAndroid.shortTitle")} - To download on Android, tap and hold{" "} - on the video, then select save. + @@ -115,16 +134,16 @@ function AndroidExplanationView({ id }: { id: string }) { function PCExplanationView({ id }: { id: string }) { const router = useOverlayRouter(id); + const { t } = useTranslation(); return ( <> router.navigate("/download")}> - Download / PC + {t("player.menus.downloads.onPc.shortTitle")} - On PC, right click the video and select{" "} - Save video as + @@ -137,27 +156,11 @@ function IOSExplanationView({ id }: { id: string }) { return ( <> router.navigate("/download")}> - Download / iOS + - To download on iOS, click{" "} - - - - , then{" "} - - Save to Files - - {" "} - . All that's left to do now is to pick a nice and cozy folder for - your video! + diff --git a/src/components/player/atoms/settings/PlaybackSettingsView.tsx b/src/components/player/atoms/settings/PlaybackSettingsView.tsx index c71c5627..23bec683 100644 --- a/src/components/player/atoms/settings/PlaybackSettingsView.tsx +++ b/src/components/player/atoms/settings/PlaybackSettingsView.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { Menu } from "@/components/player/internals/ContextMenu"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; @@ -34,6 +35,7 @@ function ButtonList(props: { } export function PlaybackSettingsView({ id }: { id: string }) { + const { t } = useTranslation(); const router = useOverlayRouter(id); const playbackRate = usePlayerStore((s) => s.mediaPlaying.playbackRate); const display = usePlayerStore((s) => s.display); @@ -50,11 +52,13 @@ export function PlaybackSettingsView({ id }: { id: string }) { return ( <> router.navigate("/")}> - Playback settings + {t("player.menus.playback.title")}
- Playback speed + + {t("player.menus.playback.speedLabel")} + router.navigate("/")}> - Quality + {t("player.menus.quality.title")} {visibleQualities.map((v) => ( @@ -76,14 +78,14 @@ export function QualityView({ id }: { id: string }) { } > - Automatic quality + {t("player.menus.quality.automaticLabel")} - You can try{" "} - router.navigate("/source")}> - switching source - {" "} - to get different quality options. + + router.navigate("/source")}> + text + + diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx index 5ecd8aec..4ab971ff 100644 --- a/src/components/player/atoms/settings/SettingsMenu.tsx +++ b/src/components/player/atoms/settings/SettingsMenu.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Toggle } from "@/components/buttons/Toggle"; import { Icon, Icons } from "@/components/Icon"; @@ -12,6 +13,7 @@ import { useSubtitleStore } from "@/stores/subtitles"; import { providers } from "@/utils/providers"; export function SettingsMenu({ id }: { id: string }) { + const { t } = useTranslation(); const router = useOverlayRouter(id); const currentQuality = usePlayerStore((s) => s.currentQuality); const selectedCaptionLanguage = usePlayerStore( @@ -26,26 +28,29 @@ export function SettingsMenu({ id }: { id: string }) { const { toggleLastUsed } = useCaptions(); const selectedLanguagePretty = selectedCaptionLanguage - ? getLanguageFromIETF(selectedCaptionLanguage) ?? "unknown" + ? getLanguageFromIETF(selectedCaptionLanguage) ?? + t("player.menus.captions.unknownLanguage") : undefined; const source = usePlayerStore((s) => s.source); return ( - Video settings + + {t("player.menus.settings.videoSection")} + router.navigate("/quality")} rightText={currentQuality ? qualityToString(currentQuality) : ""} > - Quality + {t("player.menus.settings.qualityItem")} router.navigate("/source")} rightText={sourceName} > - Video source + {t("player.menus.settings.sourceItem")} } className={source?.type === "file" ? "opacity-100" : "opacity-50"} > - Download + {t("player.menus.settings.downloadItem")} - Viewing Experience + + {t("player.menus.settings.experienceSection")} + } > - Enable Captions + {t("player.menus.settings.enableCaptions")} router.navigate("/captions")} - rightText={selectedLanguagePretty} + rightText={selectedLanguagePretty ?? undefined} > - Caption settings + {t("player.menus.settings.captionItem")} router.navigate("/playback")}> - Playback settings + {t("player.menus.settings.playbackItem")} diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index 644c573c..83c08914 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -1,4 +1,5 @@ import { ReactNode, useEffect, useMemo, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { Loading } from "@/components/layout/Loading"; import { @@ -27,13 +28,14 @@ export function EmbedOption(props: { sourceId: string; routerId: string; }) { - const unknownEmbedName = "Unknown"; + const { t } = useTranslation(); + const unknownEmbedName = t("player.menus.sources.unknownOption"); const embedName = useMemo(() => { if (!props.embedId) return unknownEmbedName; const sourceMeta = providers.getMetadata(props.embedId); return sourceMeta?.name ?? unknownEmbedName; - }, [props.embedId]); + }, [props.embedId, unknownEmbedName]); const { run, errored, loading } = useEmbedScraping( props.routerId, @@ -52,6 +54,7 @@ export function EmbedOption(props: { } export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) { + const { t } = useTranslation(); const router = useOverlayRouter(id); const { run, watching, notfound, loading, items, errored } = useSourceScraping(sourceId, id); @@ -79,21 +82,26 @@ export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) { ); else if (notfound) content = ( - - This source has no streams for this movie or show. + + {t("player.menus.sources.noStream.text")} ); else if (items?.length === 0) content = ( - - We were unable to find any embeds for this source, please try another. + + {t("player.menus.sources.noEmbeds.text")} ); else if (errored) content = ( - - We were unable to find any videos for this source. Don't come - bitchin' to us about it, just try another source. + + {t("player.menus.sources.failed.text")} ); else if (watching) @@ -123,6 +131,7 @@ export function SourceSelectionView({ id, onChoose, }: SourceSelectionViewProps) { + const { t } = useTranslation(); const router = useOverlayRouter(id); const metaType = usePlayerStore((s) => s.meta?.type); const currentSourceId = usePlayerStore((s) => s.sourceId); @@ -136,7 +145,7 @@ export function SourceSelectionView({ return ( <> router.navigate("/")}> - Sources + {t("player.menus.sources.title")} {sources.map((v) => ( diff --git a/src/components/player/base/BackLink.tsx b/src/components/player/base/BackLink.tsx index 6e42f1f4..714520ee 100644 --- a/src/components/player/base/BackLink.tsx +++ b/src/components/player/base/BackLink.tsx @@ -15,8 +15,8 @@ export function BackLink(props: { url: string }) { className="py-1 -my-1 px-2 -mx-2 tabbable rounded-lg flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium" > - {t("videoPlayer.backToHomeShort")} - {t("videoPlayer.backToHome")} + {t("player.back.short")} + {t("player.back.default")}
); diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 3b110aed..d9471893 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -163,7 +163,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { const errorDetails = getMediaErrorDetails(err); emit("error", { errorName: errorDetails.name, - message: errorDetails.message, + key: errorDetails.key, type: "htmlvideo", }); }); diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts index 1139c32a..3109180b 100644 --- a/src/components/player/display/displayInterface.ts +++ b/src/components/player/display/displayInterface.ts @@ -4,7 +4,8 @@ import { Listener } from "@/utils/events"; export type DisplayErrorType = "hls" | "htmlvideo"; export type DisplayError = { stackTrace?: string; - message: string; + message?: string; + key?: string; errorName: string; type: DisplayErrorType; }; diff --git a/src/components/player/internals/ContextMenu/Items.tsx b/src/components/player/internals/ContextMenu/Items.tsx deleted file mode 100644 index e405565d..00000000 --- a/src/components/player/internals/ContextMenu/Items.tsx +++ /dev/null @@ -1 +0,0 @@ -export function test() {} diff --git a/src/components/player/internals/ContextMenu/Misc.tsx b/src/components/player/internals/ContextMenu/Misc.tsx index 70e9555c..d1bf86c8 100644 --- a/src/components/player/internals/ContextMenu/Misc.tsx +++ b/src/components/player/internals/ContextMenu/Misc.tsx @@ -56,7 +56,7 @@ export function Paragraph(props: { return

{props.children}

; } -export function Highlight(props: { children: React.ReactNode }) { +export function Highlight(props: { children?: React.ReactNode }) { return {props.children}; } diff --git a/src/components/player/internals/HeadUpdater.tsx b/src/components/player/internals/HeadUpdater.tsx index 419976d9..3bf6feeb 100644 --- a/src/components/player/internals/HeadUpdater.tsx +++ b/src/components/player/internals/HeadUpdater.tsx @@ -16,7 +16,7 @@ export function HeadUpdater() { ); } - const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", { + const humanizedEpisodeId = t("media.episodeDisplay", { season: meta.season?.number, episode: meta.episode?.number, }); diff --git a/src/components/player/internals/KeyboardEvents.tsx b/src/components/player/internals/KeyboardEvents.tsx index 101ac3dd..b7928cab 100644 --- a/src/components/player/internals/KeyboardEvents.tsx +++ b/src/components/player/internals/KeyboardEvents.tsx @@ -29,6 +29,7 @@ export function KeyboardEvents() { mediaPlaying, isRolling, time, + router, }); useEffect(() => { dataRef.current = { @@ -41,6 +42,7 @@ export function KeyboardEvents() { mediaPlaying, isRolling, time, + router, }; }, [ setShowVolume, @@ -52,6 +54,7 @@ export function KeyboardEvents() { mediaPlaying, isRolling, time, + router, ]); useEffect(() => { @@ -92,7 +95,7 @@ export function KeyboardEvents() { dataRef.current.display?.[ dataRef.current.mediaPlaying.isPaused ? "play" : "pause" ](); - if (k === "Escape") router.close(); + if (k === "Escape") dataRef.current.router.close(); // captions if (k === "c") dataRef.current.toggleLastUsed().catch(() => {}); // ignore errors @@ -117,7 +120,7 @@ export function KeyboardEvents() { return () => { window.removeEventListener("keydown", keyEventHandler); }; - }, [router]); + }, []); return null; } diff --git a/src/components/player/internals/MetaReporter.tsx b/src/components/player/internals/MetaReporter.tsx index a9fd3b69..332deb30 100644 --- a/src/components/player/internals/MetaReporter.tsx +++ b/src/components/player/internals/MetaReporter.tsx @@ -38,6 +38,10 @@ declare global { } } +/** + * MetaReporter occasionally reports the progress to the window object at a specific spot + * This is used by the PreMid presence to get currently playing data + */ export function MetaReporter() { const meta = usePlayerStore((s) => s.meta); const progress = usePlayerStore((s) => s.progress); diff --git a/src/components/player/internals/ScrapeCard.tsx b/src/components/player/internals/ScrapeCard.tsx index bd480758..479bf2b1 100644 --- a/src/components/player/internals/ScrapeCard.tsx +++ b/src/components/player/internals/ScrapeCard.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import { StatusCircle } from "@/components/player/internals/StatusCircle"; import { Transition } from "@/components/utils/Transition"; @@ -17,9 +18,9 @@ export interface ScrapeCardProps extends ScrapeItemProps { } const statusTextMap: Partial> = { - notfound: "Doesn't have the video", - failure: "Error occured", - pending: "Checking for videos...", + notfound: "player.scraping.items.notFound", + failure: "player.scraping.items.failure", + pending: "player.scraping.items.pending", }; const statusMap: Record = { @@ -31,6 +32,7 @@ const statusMap: Record = { }; export function ScrapeItem(props: ScrapeItemProps) { + const { t } = useTranslation(); const text = statusTextMap[props.status]; const status = statusMap[props.status]; @@ -46,7 +48,7 @@ export function ScrapeItem(props: ScrapeItemProps) { {props.name}

-

{text}

+

{text ? t(text) : ""}

{props.children}
diff --git a/src/components/player/utils/handleBuffered.ts b/src/components/player/utils/handleBuffered.ts index ee19ae6a..6379d83c 100644 --- a/src/components/player/utils/handleBuffered.ts +++ b/src/components/player/utils/handleBuffered.ts @@ -1,4 +1,5 @@ export function handleBuffered(time: number, buffered: TimeRanges): number { + // TODO normalize the buffer sections into one section. they can be stitched together for (let i = 0; i < buffered.length; i += 1) { if (buffered.start(buffered.length - 1 - i) < time) { return buffered.end(buffered.length - 1 - i); diff --git a/src/components/player/utils/mediaErrorDetails.ts b/src/components/player/utils/mediaErrorDetails.ts index 646585f7..5cac36e0 100644 --- a/src/components/player/utils/mediaErrorDetails.ts +++ b/src/components/player/utils/mediaErrorDetails.ts @@ -1,35 +1,30 @@ -const mediaErrorMap: Record = { +const mediaErrorMap: Record = { 1: { name: "MEDIA_ERR_ABORTED", - message: - "The fetching of the associated resource was aborted by the user's request.", + key: "player.playbackError.errors.errorAborted", }, 2: { name: "MEDIA_ERR_NETWORK", - message: - "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", + key: "player.playbackError.errors.errorNetwork", }, 3: { name: "MEDIA_ERR_DECODE", - message: - "Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.", + key: "player.playbackError.errors.errorDecode", }, 4: { name: "MEDIA_ERR_SRC_NOT_SUPPORTED", - message: - "The associated resource or media provider object has been found to be unsuitable.", + key: "player.playbackError.errors.errorNotSupported", }, }; -export function getMediaErrorDetails(err: MediaError | null): { - name: string; - message: string; -} { +export function getMediaErrorDetails( + err: MediaError | null +): (typeof mediaErrorMap)[number] { const item = mediaErrorMap[err?.code ?? -1]; if (!item) { return { - name: "MediaError", - message: "Unknown media error occured", + name: "MEDIA_ERR_GENERIC", + key: "player.playbackError.errors.errorGenericMedia", }; } return item; diff --git a/src/pages/parts/errors/ErrorCard.tsx b/src/pages/parts/errors/ErrorCard.tsx index 85480a08..d3cf7738 100644 --- a/src/pages/parts/errors/ErrorCard.tsx +++ b/src/pages/parts/errors/ErrorCard.tsx @@ -13,8 +13,12 @@ export function ErrorCard(props: { error: DisplayError | string }) { ); const { t } = useTranslation(); - const errorMessage = - typeof props.error === "string" ? props.error : props.error.message; + let errorMessage: string | null = null; + if (typeof props.error === "string") errorMessage = props.error; + else if (props.error.key) + errorMessage = `${props.error.type}: ${t(props.error.key)}`; + else if (props.error.message) + errorMessage = `${props.error.type}: ${t(props.error.message)}`; function copyError() { if (!props.error || !navigator.clipboard) return;