Alert and Toasts

This commit is contained in:
Simon Caron
2025-01-01 21:31:04 -05:00
parent 34fc26ed18
commit e833b4bc68
15 changed files with 143 additions and 66 deletions

View File

@@ -17,6 +17,7 @@ import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomS
import {toast} from "sonner-native";
import {writeToLog} from "@/utils/log";
import { useTranslation } from "react-i18next";
import { t } from 'i18next';
export default function page() {
const navigation = useNavigation();
@@ -68,16 +69,16 @@ export default function page() {
}, [downloadedFiles]);
const deleteMovies = () => deleteFileByType("Movie")
.then(() => toast.success("Deleted all movies successfully!"))
.then(() => toast.success(t("home.downloads.toasts.deleted_all_movies_successfully")))
.catch((reason) => {
writeToLog("ERROR", reason);
toast.error("Failed to delete all movies");
toast.error(t("home.downloads.toasts.failed_to_delete_all_movies"));
});
const deleteShows = () => deleteFileByType("Episode")
.then(() => toast.success("Deleted all TV-Series successfully!"))
.then(() => toast.success(t("home.downloads.toasts.deleted_all_tvseries_successfully")))
.catch((reason) => {
writeToLog("ERROR", reason);
toast.error("Failed to delete all TV-Series");
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
});
const deleteAllMedia = async () => await Promise.all([deleteMovies(), deleteShows()])
@@ -216,15 +217,15 @@ function migration_20241124() {
const router = useRouter();
const { deleteAllFiles } = useDownload();
Alert.alert(
"New app version requires re-download",
"The new update reqires content to be downloaded again. Please remove all downloaded content and try again.",
t("home.downloads.new_app_version_requires_re_download"),
t("home.downloads.new_app_version_requires_re_download_description"),
[
{
text: "Back",
text: t("home.downloads.back"),
onPress: () => router.back(),
},
{
text: "Delete",
text: t("home.downloads.delete"),
style: "destructive",
onPress: async () => await deleteAllFiles(),
},

View File

@@ -75,7 +75,7 @@ export default function settings() {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (e) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
toast.error("Error deleting files");
toast.error(t("home.settings.toasts.error_deleting_files"));
}
};

View File

@@ -48,11 +48,13 @@ import {
import { useSharedValue } from "react-native-reanimated";
import settings from "../(tabs)/(home)/settings";
import { useSettings } from "@/utils/atoms/settings";
import { useTranslation } from "react-i18next";
export default function page() {
const videoRef = useRef<VlcPlayerViewRef>(null);
const user = useAtomValue(userAtom);
const api = useAtomValue(apiAtom);
const { t } = useTranslation();
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
const [showControls, _setShowControls] = useState(true);
@@ -160,7 +162,7 @@ export default function page() {
const { mediaSource, sessionId, url } = res;
if (!sessionId || !mediaSource || !url) {
Alert.alert("Error", "Failed to get stream url");
Alert.alert(t("player.error"), t("player.failed_to_get_stream_url"));
return null;
}
@@ -466,8 +468,8 @@ export default function page() {
onVideoError={(e) => {
console.error("Video Error:", e.nativeEvent);
Alert.alert(
"Error",
"An error occurred while playing the video. Check logs in settings."
t("player.error"),
t("player.an_error_occured_while_playing_the_video")
);
writeToLog("ERROR", "Video Error", e.nativeEvent);
}}

View File

@@ -3,10 +3,12 @@ import { useState, useCallback, useEffect, useMemo } from "react";
import { Button, Dimensions } from "react-native";
import { Alert, View } from "react-native";
import YoutubePlayer, { PLAYER_STATES } from "react-native-youtube-iframe";
import { useTranslation } from "react-i18next";
export default function page() {
const searchParams = useGlobalSearchParams();
const navigation = useNavigation();
const { t } = useTranslation();
console.log(searchParams);
const { url } = searchParams as { url: string };
@@ -20,7 +22,7 @@ export default function page() {
const onStateChange = useCallback((state: PLAYER_STATES) => {
if (state === "ended") {
setPlaying(false);
Alert.alert("video has finished playing!");
Alert.alert(t("player.video_has_finished_playing"));
}
}, []);

View File

@@ -32,6 +32,7 @@ import { MediaSourceSelector } from "./MediaSourceSelector";
import ProgressCircle from "./ProgressCircle";
import { RoundButton } from "./RoundButton";
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
import { useTranslation } from "react-i18next";
interface DownloadProps extends ViewProps {
items: BaseItemDto[];
@@ -55,6 +56,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
const [user] = useAtom(userAtom);
const [queue, setQueue] = useAtom(queueAtom);
const [settings] = useSettings();
const { t } = useTranslation();
const { processes, startBackgroundDownload, downloadedFiles } = useDownload();
const { startRemuxing } = useRemuxHlsToMp4();
@@ -160,7 +162,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
);
}
} else {
toast.error("You are not allowed to download files.");
toast.error(t("home.downloads.toasts.you_are_not_allowed_to_download_files"));
}
}, [
queue,
@@ -212,8 +214,8 @@ export const DownloadItems: React.FC<DownloadProps> = ({
if (!res) {
Alert.alert(
"Something went wrong",
"Could not get stream url from Jellyfin"
t("home.downloads.something_went_wrong"),
t("home.downloads.could_not_get_stream_url_from_jellyfin")
);
continue;
}

View File

@@ -33,6 +33,7 @@ import { Button } from "./Button";
import { SelectedOptions } from "./ItemContent";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import * as Haptics from "expo-haptics";
import { useTranslation } from "react-i18next";
interface Props extends React.ComponentProps<typeof Button> {
item: BaseItemDto;
@@ -50,6 +51,7 @@ export const PlayButton: React.FC<Props> = ({
const { showActionSheetWithOptions } = useActionSheet();
const client = useRemoteMediaClient();
const mediaStatus = useMediaStatus();
const { t } = useTranslation();
const [colorAtom] = useAtom(itemThemeColorAtom);
const api = useAtomValue(apiAtom);
@@ -131,8 +133,8 @@ export const PlayButton: React.FC<Props> = ({
if (!data?.url) {
console.warn("No URL returned from getStreamUrl", data);
Alert.alert(
"Client error",
"Could not create stream for Chromecast"
t("player.client_error"),
t("player.could_not_create_stream_for_chromecast")
);
return;
}

View File

@@ -22,13 +22,12 @@ import { Button } from "../Button";
import { Image } from "expo-image";
import { useMemo } from "react";
import { storage } from "@/utils/mmkv";
import { useTranslation } from "react-i18next";
import { t } from "i18next";
interface Props extends ViewProps {}
export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
const { processes } = useDownload();
const { t } = useTranslation();
if (processes?.length === 0)
return (
<View {...props} className="bg-neutral-900 p-4 rounded-2xl">
@@ -84,11 +83,11 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
}
},
onSuccess: () => {
toast.success("Download canceled");
toast.success(t("home.downloads.toasts.download_cancelled"));
},
onError: (e) => {
console.error(e);
toast.error("Could not cancel download");
toast.error(t("home.downloads.toasts.could_not_cancel_download"));
},
});

View File

@@ -20,6 +20,7 @@ import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { Image } from "expo-image";
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import { Loader } from "../Loader";
import { t } from "i18next";
const JellyseerrSeasonEpisodes: React.FC<{
details: TvDetails;
@@ -155,13 +156,13 @@ const JellyseerrSeasons: React.FC<{
const promptRequestAll = useCallback(
() =>
Alert.alert("Confirm", "Are you sure you want to request all seasons?", [
Alert.alert(t("jellyseerr.confirm"), t("jellyseerr.are_you_sure_you_want_to_request_all_seasons"), [
{
text: "Cancel",
text: t("jellyseerr.cancel"),
style: "cancel",
},
{
text: "Yes",
text: t("jellyseerr.yes"),
onPress: requestAll,
},
]),

View File

@@ -50,7 +50,7 @@ export const JellyseerrSettings = () => {
updateSettings({ jellyseerrServerUrl });
},
onError: () => {
toast.error("Failed to login");
toast.error(t("jellyseerr.failed_to_login"));
},
onSettled: () => {
setJellyseerrPassword(undefined);
@@ -124,16 +124,16 @@ export const JellyseerrSettings = () => {
) : (
<View className="flex flex-col rounded-xl overflow-hidden p-4 bg-neutral-900">
<Text className="text-xs text-red-600 mb-2">
{t("home.settings.jellyseer.jellyseer_warning")}
{t("home.settings.jellyseerr.jellyseerr_warning")}
</Text>
<Text className="font-bold mb-1">{t("home.settings.jellyseer.server_url")}</Text>
<Text className="font-bold mb-1">{t("home.settings.jellyseerr.server_url")}</Text>
<View className="flex flex-col shrink mb-2">
<Text className="text-xs text-gray-600">
{t("home.settings.jellyseer.server_url_hint")}
{t("home.settings.jellyseerr.server_url_hint")}
</Text>
</View>
<Input
placeholder={t("home.settings.jellyseer.server_url_placeholder")}
placeholder={t("home.settings.jellyseerr.server_url_placeholder")}
value={settings?.jellyseerrServerUrl ?? jellyseerrServerUrl}
defaultValue={
settings?.jellyseerrServerUrl ?? jellyseerrServerUrl
@@ -163,7 +163,7 @@ export const JellyseerrSettings = () => {
marginBottom: 8,
}}
>
{promptForJellyseerrPass ? t("home.settings.jellyseer.clear_button") : t("home.settings.jellyseer.save_button")}
{promptForJellyseerrPass ? t("home.settings.jellyseerr.clear_button") : t("home.settings.jellyseerr.save_button")}
</Button>
<View
@@ -172,11 +172,11 @@ export const JellyseerrSettings = () => {
opacity: promptForJellyseerrPass ? 1 : 0.5,
}}
>
<Text className="font-bold mb-2">{t("home.settings.jellyseer.password")}</Text>
<Text className="font-bold mb-2">{t("home.settings.jellyseerr.password")}</Text>
<Input
autoFocus={true}
focusable={true}
placeholder={t("home.settings.jellyseer.password_placeholder", {username: user?.Name})}
placeholder={t("home.settings.jellyseerr.password_placeholder", {username: user?.Name})}
value={jellyseerrPassword}
keyboardType="default"
secureTextEntry={true}
@@ -196,7 +196,7 @@ export const JellyseerrSettings = () => {
className="h-12 mt-2"
onPress={() => loginToJellyseerrMutation.mutate()}
>
{t("home.settings.jellyseer.login_button")}
{t("home.settings.jellyseerr.login_button")}
</Button>
</View>
</View>

View File

@@ -77,10 +77,10 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
if (settings?.autoDownload === true && !registered) {
registerBackgroundFetchAsync();
toast.success("Background downloads enabled");
toast.success(t("home.settings.toasts.background_downloads_enabled"));
} else if (settings?.autoDownload === false && registered) {
unregisterBackgroundFetchAsync();
toast.info("Background downloads disabled");
toast.info(t("home.settings.toasts.background_downloads_disabled"));
} else if (settings?.autoDownload === true && registered) {
// Don't to anything
} else if (settings?.autoDownload === false && !registered) {
@@ -654,8 +654,8 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
deviceId: await getOrSetDeviceId(),
});
if (res) {
toast.success("Connected");
} else toast.error("Could not connect");
toast.success(t("home.settings.toasts.connected"));
} else toast.error(t("home.settings.toasts.could_not_connect"));
}}
>
{t("home.settings.downloads.save_button")}

View File

@@ -28,6 +28,7 @@ import Issue from "@/utils/jellyseerr/server/entity/Issue";
import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import { writeErrorLog } from "@/utils/log";
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { t } from "i18next";
interface SearchParams {
query: string;
@@ -113,7 +114,7 @@ export class JellyseerrApi {
if (inRange(status, 200, 299)) {
if (data.version < "2.0.0") {
const error =
"Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0";
t("jellyseerr.toasts.jellyseer_does_not_meet_requirements");
toast.error(error);
throw Error(error);
}
@@ -127,7 +128,7 @@ export class JellyseerrApi {
requiresPass: true,
};
}
toast.error(`Jellyseerr test failed. Please try again.`);
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed"));
writeErrorLog(
`Jellyseerr returned a ${status} for url:\n` +
response.config.url +
@@ -140,7 +141,7 @@ export class JellyseerrApi {
};
})
.catch((e) => {
const msg = "Failed to test jellyseerr server url";
const msg = t("jellyseerr.toasts.failed_to_test_jellyseerr_server_url");
toast.error(msg);
console.error(msg, e);
return {
@@ -259,7 +260,7 @@ export class JellyseerrApi {
const issue = response.data;
if (issue.status === IssueStatus.OPEN) {
toast.success("Issue submitted!");
toast.success(t("jellyseerr.toasts.issue_submitted"));
}
return issue;
});
@@ -342,13 +343,13 @@ export const useJellyseerr = () => {
switch (mediaRequest.status) {
case MediaRequestStatus.PENDING:
case MediaRequestStatus.APPROVED:
toast.success(`Requested ${title}!`);
toast.success(t("jellyseerr.toasts.requested_item", {item: title}));
break;
case MediaRequestStatus.DECLINED:
toast.error(`You don't have permission to request!`);
toast.error(t("jellyseerr.toasts.you_dont_have_permission_to_request"));
break;
case MediaRequestStatus.FAILED:
toast.error(`Something went wrong requesting media!`);
toast.error(t("jellyseerr.toasts.something_went_wrong_requesting_media"));
break;
}
});

View File

@@ -18,6 +18,7 @@ import useDownloadHelper from "@/utils/download";
import { Api } from "@jellyfin/sdk";
import { useSettings } from "@/utils/atoms/settings";
import { JobStatus } from "@/utils/optimize-server";
import { useTranslation } from "react-i18next";
const createFFmpegCommand = (url: string, output: string) => [
"-y", // overwrite output files without asking
@@ -49,6 +50,7 @@ export const useRemuxHlsToMp4 = () => {
const api = useAtomValue(apiAtom);
const router = useRouter();
const queryClient = useQueryClient();
const { t } = useTranslation();
const [settings] = useSettings();
const { saveImage } = useImageStorage();
@@ -84,7 +86,7 @@ export const useRemuxHlsToMp4 = () => {
queryKey: ["downloadedItems"],
});
saveDownloadedItemInfo(item, stat.getSize());
toast.success("Download completed");
toast.success(t("home.downloads.toasts.download_completed"));
}
setProcesses((prev) => {
@@ -144,7 +146,7 @@ export const useRemuxHlsToMp4 = () => {
// First lets save any important assets we want to present to the user offline
await onSaveAssets(api, item);
toast.success(`Download started for ${item.Name}`, {
toast.success(t("home.downloads.toasts.download_started_for", {item: item.Name}), {
action: {
label: "Go to download",
onClick: () => {

View File

@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { Alert } from "react-native";
import { useRouter } from "expo-router";
import { useWebSocketContext } from "@/providers/WebSocketProvider";
import { useTranslation } from "react-i18next";
interface UseWebSocketProps {
isPlaying: boolean;
@@ -18,6 +19,7 @@ export const useWebSocket = ({
}: UseWebSocketProps) => {
const router = useRouter();
const { ws } = useWebSocketContext();
const { t } = useTranslation();
useEffect(() => {
if (!ws) return;
@@ -40,7 +42,7 @@ export const useWebSocket = ({
console.log("Command ~ DisplayMessage");
const title = json?.Data?.Arguments?.Header;
const body = json?.Data?.Arguments?.Text;
Alert.alert("Message from server: " + title, body);
Alert.alert(t("player.message_from_server", {message: title}), body);
}
};

View File

@@ -50,6 +50,7 @@ import useDownloadHelper from "@/utils/download";
import { FileInfo } from "expo-file-system";
import * as Haptics from "expo-haptics";
import * as Application from "expo-application";
import { useTranslation } from "react-i18next";
export type DownloadedItem = {
item: Partial<BaseItemDto>;
@@ -68,6 +69,7 @@ const DownloadContext = createContext<ReturnType<
function useDownloadProvider() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const [settings] = useSettings();
const router = useRouter();
const [api] = useAtom(apiAtom);
@@ -137,9 +139,9 @@ function useDownloadProvider() {
if (settings.autoDownload) {
startDownload(job);
} else {
toast.info(`${job.item.Name} is ready to be downloaded`, {
toast.info(t("home.downloads.toasts.item_is_ready_to_be_downloaded",{item: job.item.Name}), {
action: {
label: "Go to downloads",
label: t("home.downloads.toasts.go_to_downloads"),
onClick: () => {
router.push("/downloads");
toast.dismiss();
@@ -222,9 +224,9 @@ function useDownloadProvider() {
},
});
toast.info(`Download started for ${process.item.Name}`, {
toast.info(t("home.downloads.toasts.download_stated_for_item", {item: process.item.Name}), {
action: {
label: "Go to downloads",
label: t("home.downloads.toasts.go_to_downloads"),
onClick: () => {
router.push("/downloads");
toast.dismiss();
@@ -273,10 +275,10 @@ function useDownloadProvider() {
process.item,
doneHandler.bytesDownloaded
);
toast.success(`Download completed for ${process.item.Name}`, {
toast.success(t("home.downloads.toasts.download_completed_for_item", {item: process.item.Name}), {
duration: 3000,
action: {
label: "Go to downloads",
label: t("home.downloads.toasts.go_to_downloads"),
onClick: () => {
router.push("/downloads");
toast.dismiss();
@@ -298,7 +300,7 @@ function useDownloadProvider() {
if (error.errorCode === 404) {
errorMsg = "File not found on server";
}
toast.error(`Download failed for ${process.item.Name} - ${errorMsg}`);
toast.error(t("home.downloads.toasts.download_failed_for_item", {item: process.item.Name, error: errorMsg}));
writeToLog("ERROR", `Download failed for ${process.item.Name}`, {
error,
processDetails: {
@@ -355,9 +357,9 @@ function useDownloadProvider() {
throw new Error("Failed to start optimization job");
}
toast.success(`Queued ${item.Name} for optimization`, {
toast.success(t("home.downloads.toasts.queued_item_for_optimization", {item: item.Name}), {
action: {
label: "Go to download",
label: t("home.downloads.toasts.go_to_downloads"),
onClick: () => {
router.push("/downloads");
toast.dismiss();
@@ -375,21 +377,21 @@ function useDownloadProvider() {
headers: error.response?.headers,
});
toast.error(
`Failed to start download for ${item.Name}: ${error.message}`
t("home.downloads.toasts.failed_to_start_download_for_item", {item: item.Name, message: error.message})
);
if (error.response) {
toast.error(
`Server responded with status ${error.response.status}`
t("home.downloads.toasts.server_responded_with_status", {statusCode: error.response.status})
);
} else if (error.request) {
toast.error("No response received from server");
t("home.downloads.toasts.no_response_received_from_server");
} else {
toast.error("Error setting up the request");
}
} else {
console.error("Non-Axios error:", error);
toast.error(
`Failed to start download for ${item.Name}: Unexpected error`
t("home.downloads.toasts.failed_to_start_download_for_item_unexpected_error", {item: item.Name})
);
}
}
@@ -405,11 +407,11 @@ function useDownloadProvider() {
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] }),
])
.then(() =>
toast.success("All files, folders, and jobs deleted successfully")
toast.success(t("home.downloads.toasts.all_files_folders_jobs_deleted"))
)
.catch((reason) => {
console.error("Failed to delete all files, folders, and jobs:", reason);
toast.error("An error occurred while deleting files and jobs");
toast.error(t("home.downloads.toasts.an_error_occured_while_deleting_files_and_jobs"));
});
};

View File

@@ -107,8 +107,8 @@
"optimized_versions_server_hint": "Set the URL for the optimized versions server for downloads.",
"save_button": "Save"
},
"jellyseer": {
"jellyseer_warning": "This integration is in its early stages. Expect things to change.",
"jellyseerr": {
"jellyseerr_warning": "This integration is in its early stages. Expect things to change.",
"server_url": "Server URL",
"server_url_hint": "Example: http(s)://your-host.url\n(add port if required)",
"server_url_placeholder": "Jellyseerr URL...",
@@ -134,6 +134,13 @@
"app_language": "App language",
"app_language_description": "Select the language for the app.",
"system": "System"
},
"toasts":{
"error_deleting_files": "Error deleting files",
"background_downloads_enabled": "Background downloads enabled",
"background_downloads_disabled": "Background downloads disabled",
"connected": "Connected",
"could_not_connect": "Could not connect"
}
},
"downloads": {
@@ -150,7 +157,36 @@
"active_download": "Active download",
"no_active_downloads": "No active downloads",
"active_downloads": "Active downloads",
"toasts": {}
"new_app_version_requires_re_download": "New app version requires re-download",
"new_app_version_requires_re_download_description": "The new update reqires content to be downloaded again. Please remove all downloaded content and try again.",
"back": "Back",
"delete": "Delete",
"something_went_wrong": "Something went wrong",
"could_not_get_stream_url_from_jellyfin": "Could not get stream URL from Jellyfin",
"toasts": {
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
"deleted_all_movies_successfully": "Deleted all movies successfully!",
"failed_to_delete_all_movies": "Failed to delete all movies",
"deleted_all_tvseries_successfully": "Deleted all TV-Series successfully!",
"failed_to_delete_all_tvseries": "Failed to delete all TV-Series",
"download_cancelled": "Download cancelled",
"could_not_cancel_download": "Could not cancel download",
"download_completed": "Download completed",
"download_started_for": "Download started for {{item}}",
"item_is_ready_to_be_downloaded": "{{item}} is ready to be downloaded",
"download_stated_for_item": "Download started for {{item}}",
"download_failed_for_item": "Download failed for {{item}} - {{error}}",
"download_completed_for_item": "Download completed for {{item}}",
"queued_item_for_optimization": "Queued {{item}} for optimization",
"failed_to_start_download_for_item": "Failed to start download for {{item}}: {{message}}",
"server_responded_with_status_code": "Server responded with status {{statusCode}}",
"no_response_received_from_server": "No response received from server",
"error_setting_up_the_request": "Error setting up the request",
"failed_to_start_download_for_item_unexpected_error": "Failed to start download for {{item}}: Unexpected error",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders and jobs deleted successfully",
"an_error_occured_while_deleting_files_and_jobs": "An error occurred while deleting files and jobs",
"go_to_downloads": "Go to downloads"
}
}
},
"search": {
@@ -164,6 +200,31 @@
"no_results": "No results",
"no_libraries_found": "No libraries found"
},
"player": {
"error": "Error",
"failed_to_get_stream_url": "Failed to get stream URL",
"an_error_occured_while_playing_the_video": "An error occurred while playing the video. Check logs in settings.",
"client_error": "Client error",
"could_not_create_stream_for_chromecast": "Could not create stream for Chromecast",
"message_from_server": "Message from server: {{message}}",
"video_has_finished_playing": "Video has finished playing!"
},
"jellyseerr":{
"confirm": "Confirm",
"cancel": "Cancel",
"yes": "Yes",
"are_you_sure_you_want_to_request_all_seasons": "Are you sure you want to request all seasons?",
"failed_to_login": "Failed to login",
"toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0",
"jellyseerr_test_failed": "Jellyseerr test failed. Please try again.",
"failed_to_test_jellyseerr_server_url": "Failed to test jellyseerr server url",
"issue_submitted": "Issue submitted!",
"requested_item": "Requested {{item}}!",
"you_dont_have_permission_to_request": "You don't have permission to request!",
"something_went_wrong_requesting_media": "Something went wrong requesting media!"
}
},
"tabs": {
"home": "Home",
"search": "Search",