diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index a9a2e2fbb..905d02bbe 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -645,7 +645,7 @@ export default function SettingsTV() { formatValue={(v) => `${v.toFixed(1)}x`} /> { const newValue = Math.max( @@ -663,11 +663,11 @@ export default function SettingsTV() { }} /> showOptions({ - title: "Horizontal Alignment", + title: t("home.settings.subtitles.mpv_subtitle_align_x"), options: alignXOptions, onSelect: (value) => updateSettings({ @@ -677,11 +677,11 @@ export default function SettingsTV() { } /> showOptions({ - title: "Vertical Alignment", + title: t("home.settings.subtitles.mpv_subtitle_align_y"), options: alignYOptions, onSelect: (value) => updateSettings({ diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx index f268e0b24..27b035768 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx @@ -89,7 +89,7 @@ export default function ArtistsScreen() { return ( - Missing music library id. + {t("music.missing_library_id")} ); diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx index 85b4be5fd..db06f7273 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/playlists.tsx @@ -122,7 +122,7 @@ export default function PlaylistsScreen() { return ( - Missing music library id. + {t("music.missing_library_id")} ); diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx index 81c2272f8..c19d05595 100644 --- a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx +++ b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/suggestions.tsx @@ -226,7 +226,7 @@ export default function SuggestionsScreen() { return ( - Missing music library id. + {t("music.missing_library_id")} ); diff --git a/app/(auth)/now-playing.tsx b/app/(auth)/now-playing.tsx index 934175fb8..d0051ae52 100644 --- a/app/(auth)/now-playing.tsx +++ b/app/(auth)/now-playing.tsx @@ -14,6 +14,7 @@ import React, { useRef, useState, } from "react"; +import { useTranslation } from "react-i18next"; import { ActivityIndicator, Dimensions, @@ -72,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(); @@ -230,7 +232,9 @@ export default function NowPlayingScreen() { paddingBottom: Platform.OS === "android" ? insets.bottom : 0, }} > - No track playing + + {t("music.no_track_playing")} + ); @@ -267,7 +271,7 @@ export default function NowPlayingScreen() { : "text-neutral-500" } > - Now Playing + {t("music.now_playing")} = ({ onRemoveFromQueue, onReorderQueue, }) => { + const { t } = useTranslation(); const renderQueueItem = useCallback( ({ item, drag, isActive, getIndex }: RenderItemParams) => { const index = getIndex() ?? 0; @@ -831,13 +836,15 @@ const QueueView: React.FC = ({ ListHeaderComponent={ - {history.length > 0 ? "Playing from queue" : "Up next"} + {history.length > 0 + ? t("music.playing_from_queue") + : t("music.up_next")} } ListEmptyComponent={ - Queue is empty + {t("music.queue_empty")} } /> diff --git a/app/(auth)/tv-subtitle-modal.tsx b/app/(auth)/tv-subtitle-modal.tsx index ed5d24d9c..92c9c549c 100644 --- a/app/(auth)/tv-subtitle-modal.tsx +++ b/app/(auth)/tv-subtitle-modal.tsx @@ -192,6 +192,7 @@ const SubtitleResultCard = React.forwardRef< >(({ result, hasTVPreferredFocus, isDownloading, onPress }, ref) => { const { focused, handleFocus, handleBlur, animatedStyle } = useTVFocusAnimation({ scaleAmount: 1.03 }); + const { t } = useTranslation(); return ( - Hash Match + {t("player.hash_match")} )} diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 7fd022ff8..c3014be24 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -1,17 +1,20 @@ import { Link, Stack } from "expo-router"; +import { useTranslation } from "react-i18next"; import { StyleSheet } from "react-native"; import { ThemedText } from "@/components/ThemedText"; import { ThemedView } from "@/components/ThemedView"; export default function NotFoundScreen() { + const { t } = useTranslation(); + return ( <> - + - This screen doesn't exist. + {t("not_found.title")} - Go to home screen! + {t("not_found.go_home")} diff --git a/components/PlatformDropdown.tsx b/components/PlatformDropdown.tsx index 5487393dd..3f37b0c33 100644 --- a/components/PlatformDropdown.tsx +++ b/components/PlatformDropdown.tsx @@ -1,6 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; 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"; @@ -209,6 +210,7 @@ const PlatformDropdownComponent = ({ expoUIConfig, bottomSheetConfig, }: PlatformDropdownProps) => { + const { t } = useTranslation(); const { showModal, hideModal, isVisible } = useGlobalModal(); // Handle controlled open state for Android @@ -380,7 +382,7 @@ const PlatformDropdownComponent = ({ return ( - {trigger || Open Menu} + {trigger || {t("common.open_menu")}} ); }; diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index 462705d63..b9ed11c75 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -502,8 +502,8 @@ export const PlayButton: React.FC = ({ return ( diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index c8b6b76e3..d6793453d 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -2,6 +2,7 @@ import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useAtom } from "jotai"; import { useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { TouchableOpacity, View } from "react-native"; import Animated, { Easing, @@ -36,6 +37,7 @@ export const PlayButton: React.FC = ({ colors, ...props }: Props) => { + const { t } = useTranslation(); const [globalColorAtom] = useAtom(itemThemeColorAtom); // Use colors prop if provided, otherwise fallback to global atom @@ -168,8 +170,8 @@ export const PlayButton: React.FC = ({ return ( = ({ const [modalVisible, setModalVisible] = useState(false); const api = useAtomValue(apiAtom); const { sessions, isLoading } = useAllSessions({} as useSessionsProps); + const { t } = useTranslation(); const handlePlayInSession = async (sessionId: string) => { if (!api || !item.Id) return; @@ -65,7 +67,9 @@ export const PlayInRemoteSessionButton: React.FC = ({ - Select Session + + {t("home.sessions.select_session")} + setModalVisible(false)}> @@ -78,7 +82,7 @@ export const PlayInRemoteSessionButton: React.FC = ({ ) : !sessions || sessions.length === 0 ? ( - No active sessions found + {t("home.sessions.no_active_sessions")} ) : ( = ({ {session.NowPlayingItem && ( - Now playing:{" "} + {t("home.sessions.now_playing")}{" "} {session.NowPlayingItem.SeriesName ? `${session.NowPlayingItem.SeriesName} :` : ""} diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx index c67f60583..2d1b85a0c 100644 --- a/components/downloads/DownloadCard.tsx +++ b/components/downloads/DownloadCard.tsx @@ -1,7 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import { Image } from "expo-image"; -import { t } from "i18next"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { ActivityIndicator, TouchableOpacity, @@ -35,6 +35,7 @@ interface DownloadCardProps extends TouchableOpacityProps { } export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { + const { t } = useTranslation(); const { cancelDownload } = useDownload(); const router = useRouter(); const queryClient = useNetworkAwareQueryClient(); @@ -173,7 +174,9 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { {isTranscoding && ( - Transcoding + + {t("home.downloads.transcoding")} + )} diff --git a/components/livetv/TVGuideProgramCell.tsx b/components/livetv/TVGuideProgramCell.tsx index e8287132e..27010dae7 100644 --- a/components/livetv/TVGuideProgramCell.tsx +++ b/components/livetv/TVGuideProgramCell.tsx @@ -1,5 +1,6 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; 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"; @@ -22,6 +23,7 @@ export const TVGuideProgramCell: React.FC = ({ disabled = false, refSetter, }) => { + const { t } = useTranslation(); const typography = useScaledTVTypography(); const { focused, handleFocus, handleBlur } = useTVFocusAnimation({ scaleAmount: 1, @@ -68,7 +70,7 @@ export const TVGuideProgramCell: React.FC = ({ - LIVE + {t("player.live")} )} diff --git a/components/livetv/TVLiveTVPage.tsx b/components/livetv/TVLiveTVPage.tsx index a3f3ed45d..4ecfeb802 100644 --- a/components/livetv/TVLiveTVPage.tsx +++ b/components/livetv/TVLiveTVPage.tsx @@ -235,7 +235,7 @@ export const TVLiveTVPage: React.FC = () => { marginBottom: 24, }} > - Live TV + {t("live_tv.title")} {/* Tab Bar */} diff --git a/components/search/TVSearchTabBadges.tsx b/components/search/TVSearchTabBadges.tsx index d15d43ce2..1bac2b017 100644 --- a/components/search/TVSearchTabBadges.tsx +++ b/components/search/TVSearchTabBadges.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Animated, Pressable, View } from "react-native"; import { Text } from "@/components/common/Text"; import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; @@ -88,6 +89,8 @@ export const TVSearchTabBadges: React.FC = ({ showDiscover, disabled = false, }) => { + const { t } = useTranslation(); + if (!showDiscover) { return null; } @@ -101,13 +104,13 @@ export const TVSearchTabBadges: React.FC = ({ }} > setSearchType("Library")} disabled={disabled} /> setSearchType("Discover")} disabled={disabled} diff --git a/components/settings/MpvSubtitleSettings.tsx b/components/settings/MpvSubtitleSettings.tsx index 4ef2a8001..c481a0803 100644 --- a/components/settings/MpvSubtitleSettings.tsx +++ b/components/settings/MpvSubtitleSettings.tsx @@ -1,5 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { Platform, Switch, View, type ViewProps } from "react-native"; import { Stepper } from "@/components/inputs/Stepper"; import { Text } from "../common/Text"; @@ -17,20 +18,21 @@ export const MpvSubtitleSettings: React.FC = ({ ...props }) => { const isTv = Platform.isTV; const media = useMedia(); const { settings, updateSettings } = media; + const { t } = useTranslation(); const alignXOptions: AlignX[] = ["left", "center", "right"]; const alignYOptions: AlignY[] = ["top", "center", "bottom"]; const alignXLabels: Record = { - left: "Left", - center: "Center", - right: "Right", + left: t("home.settings.subtitles.align.left"), + center: t("home.settings.subtitles.align.center"), + right: t("home.settings.subtitles.align.right"), }; const alignYLabels: Record = { - top: "Top", - center: "Center", - bottom: "Bottom", + top: t("home.settings.subtitles.align.top"), + center: t("home.settings.subtitles.align.center"), + bottom: t("home.settings.subtitles.align.bottom"), }; const alignXOptionGroups = useMemo(() => { @@ -60,16 +62,18 @@ export const MpvSubtitleSettings: React.FC = ({ ...props }) => { return ( - Advanced subtitle customization for MPV player + {t("home.settings.subtitles.mpv_settings_description")} } > {!isTv && ( <> - + = ({ ...props }) => { /> - + = ({ ...props }) => { /> } - title='Horizontal Alignment' + title={t("home.settings.subtitles.mpv_subtitle_align_x")} /> - + = ({ ...props }) => { /> } - title='Vertical Alignment' + title={t("home.settings.subtitles.mpv_subtitle_align_y")} /> )} - + @@ -131,7 +135,7 @@ export const MpvSubtitleSettings: React.FC = ({ ...props }) => { {settings.mpvSubtitleBackgroundEnabled && ( - + - Hash Match + {t("player.hash_match")} )} {result.hearingImpaired && ( diff --git a/components/video-player/controls/BottomControls.tsx b/components/video-player/controls/BottomControls.tsx index 81c77ab8c..ec87b0257 100644 --- a/components/video-player/controls/BottomControls.tsx +++ b/components/video-player/controls/BottomControls.tsx @@ -183,7 +183,7 @@ export const BottomControls: FC = ({ {/* Smart Skip Credits behavior: - Show "Skip Credits" if there's content after credits OR no next episode @@ -193,7 +193,7 @@ export const BottomControls: FC = ({ showSkipCreditButton && (hasContentAfterCredits || !nextItem) } onPress={skipCredit} - buttonText='Skip Credits' + buttonText={t("player.skip_credits")} /> {settings.autoPlayNextEpisode !== false && (settings.maxAutoPlayEpisodeCount.value === -1 || diff --git a/components/video-player/controls/ContinueWatchingOverlay.tsx b/components/video-player/controls/ContinueWatchingOverlay.tsx index 26f82484d..bf813b5cc 100644 --- a/components/video-player/controls/ContinueWatchingOverlay.tsx +++ b/components/video-player/controls/ContinueWatchingOverlay.tsx @@ -27,7 +27,7 @@ const ContinueWatchingOverlay: React.FC = ({ } > - Are you still watching ? + {t("player.still_watching")}