diff --git a/app/(auth)/now-playing.tsx b/app/(auth)/now-playing.tsx index c1f0dc4b..d0051ae5 100644 --- a/app/(auth)/now-playing.tsx +++ b/app/(auth)/now-playing.tsx @@ -6,7 +6,6 @@ import type { MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; -import { t } from "i18next"; import { useAtom } from "jotai"; import React, { useCallback, @@ -15,6 +14,7 @@ import React, { useRef, useState, } from "react"; +import { useTranslation } from "react-i18next"; import { ActivityIndicator, Dimensions, @@ -73,6 +73,7 @@ const ARTWORK_SIZE = SCREEN_WIDTH - 80; type ViewMode = "player" | "queue"; export default function NowPlayingScreen() { + const { t } = useTranslation(); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const router = useRouter(); @@ -721,6 +722,7 @@ const QueueView: React.FC = ({ onRemoveFromQueue, onReorderQueue, }) => { + const { t } = useTranslation(); const renderQueueItem = useCallback( ({ item, drag, isActive, getIndex }: RenderItemParams) => { const index = getIndex() ?? 0; diff --git a/components/PlatformDropdown.tsx b/components/PlatformDropdown.tsx index e9f5c397..3f37b0c3 100644 --- a/components/PlatformDropdown.tsx +++ b/components/PlatformDropdown.tsx @@ -1,7 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; -import { t } from "i18next"; import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; @@ -210,6 +210,7 @@ const PlatformDropdownComponent = ({ expoUIConfig, bottomSheetConfig, }: PlatformDropdownProps) => { + const { t } = useTranslation(); const { showModal, hideModal, isVisible } = useGlobalModal(); // Handle controlled open state for Android diff --git a/components/livetv/TVGuideProgramCell.tsx b/components/livetv/TVGuideProgramCell.tsx index 446bacb2..27010dae 100644 --- a/components/livetv/TVGuideProgramCell.tsx +++ b/components/livetv/TVGuideProgramCell.tsx @@ -1,6 +1,6 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { t } from "i18next"; import React from "react"; +import { useTranslation } from "react-i18next"; import { Animated, Pressable, StyleSheet, View } from "react-native"; import { Text } from "@/components/common/Text"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; @@ -23,6 +23,7 @@ export const TVGuideProgramCell: React.FC = ({ disabled = false, refSetter, }) => { + const { t } = useTranslation(); const typography = useScaledTVTypography(); const { focused, handleFocus, handleBlur } = useTVFocusAnimation({ scaleAmount: 1, diff --git a/components/tv/TVPosterCard.tsx b/components/tv/TVPosterCard.tsx index 1f20e91c..669c0a2d 100644 --- a/components/tv/TVPosterCard.tsx +++ b/components/tv/TVPosterCard.tsx @@ -1,9 +1,9 @@ import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; -import { t } from "i18next"; import { useAtomValue } from "jotai"; import React, { useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Animated, Easing, @@ -107,6 +107,7 @@ export const TVPosterCard: React.FC = ({ scaleAmount = 1.05, imageUrlGetter, }) => { + const { t } = useTranslation(); const api = useAtomValue(apiAtom); const posterSizes = useScaledTVPosterSizes(); const typography = useScaledTVTypography(); diff --git a/components/video-player/controls/TechnicalInfoOverlay.tsx b/components/video-player/controls/TechnicalInfoOverlay.tsx index 404e5f06..d56b448a 100644 --- a/components/video-player/controls/TechnicalInfoOverlay.tsx +++ b/components/video-player/controls/TechnicalInfoOverlay.tsx @@ -344,8 +344,9 @@ export const TechnicalInfoOverlay: FC = memo( )} {info?.cacheSeconds !== undefined && ( - {t("player.technical_info.buffer")} {info.cacheSeconds.toFixed(1)} - s + {t("player.technical_info.buffer_seconds", { + seconds: info.cacheSeconds.toFixed(1), + })} )} {info?.voDriver && ( diff --git a/hooks/useAppRouter.ts b/hooks/useAppRouter.ts index ce255fe3..82bc7b1b 100644 --- a/hooks/useAppRouter.ts +++ b/hooks/useAppRouter.ts @@ -31,9 +31,10 @@ export function useAppRouter() { const push = useCallback( (href: Parameters[0]) => { - // Duplicate-screen guard: a push blurs the source screen synchronously in - // the navigation state (only the native render is slow). A second tap then - // sees an unfocused screen and is dropped. Resets automatically on return. + // Rapid-push guard: a push blurs the source screen synchronously in the + // navigation state (only the native render is slow). Any further push from + // this screen — duplicate or not — is dropped until focus returns, so taps + // fired before the pushed screen renders can't stack screens. // No navigation context => nothing to guard (deep-link pushes from root). if (navigation?.isFocused?.() === false) return; if (typeof href === "string") { diff --git a/translations/en.json b/translations/en.json index c4030ff3..1cad697a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -631,7 +631,7 @@ "audio": "Audio:", "subtitle": "Subtitle:", "bitrate": "Bitrate:", - "buffer": "Buffer:", + "buffer_seconds": "Buffer: {{seconds}}s", "vo": "VO:", "dropped_frames": "Dropped: {{count}} frames", "loading": "Loading..."