From 0d47c8d43a5ec24de1277585e44b7bbc0951540d Mon Sep 17 00:00:00 2001 From: Gauvain Date: Wed, 10 Jun 2026 22:29:16 +0200 Subject: [PATCH] feat(i18n): localize hardcoded UI strings and fix misspelled keys Move remaining hardcoded English strings (player menus, technical-info overlay, music/now-playing, live TV, TV search badges, MPV subtitle settings, accessibility labels, not-found screen, session picker) to en.json, and correct misspelled keys (occured -> occurred, autorized -> authorized, liraries -> libraries, jellyseer -> jellyseerr) along with their usages. --- app/(auth)/(tabs)/(home)/settings.tv.tsx | 10 +-- .../appearance/hide-libraries/page.tsx | 2 +- .../(home)/settings/hide-libraries/page.tsx | 2 +- .../(libraries)/music/[libraryId]/artists.tsx | 2 +- .../music/[libraryId]/playlists.tsx | 2 +- .../music/[libraryId]/suggestions.tsx | 2 +- app/(auth)/now-playing.tsx | 13 ++-- app/(auth)/player/direct-player.tsx | 2 +- app/(auth)/tv-subtitle-modal.tsx | 3 +- app/+not-found.tsx | 9 ++- components/PlatformDropdown.tsx | 3 +- components/PlayButton.tsx | 4 +- components/PlayButton.tv.tsx | 6 +- components/PlayInRemoteSession.tsx | 10 ++- components/downloads/DownloadCard.tsx | 4 +- components/livetv/TVGuideProgramCell.tsx | 3 +- components/livetv/TVLiveTVPage.tsx | 2 +- components/login/TVLogin.tsx | 8 +-- components/search/TVSearchTabBadges.tsx | 7 +- components/settings/MpvSubtitleSettings.tsx | 34 +++++---- components/settings/QuickConnect.tsx | 2 +- components/tv/TVPosterCard.tsx | 3 +- components/tv/TVSubtitleResultCard.tsx | 4 +- .../video-player/controls/BottomControls.tsx | 4 +- .../controls/ContinueWatchingOverlay.tsx | 2 +- .../video-player/controls/HeaderControls.tsx | 6 +- .../controls/TechnicalInfoOverlay.tsx | 24 ++++--- .../controls/VideoScalingModeSelector.tsx | 9 ++- .../controls/dropdown/DropdownView.tsx | 19 ++--- .../controls/hooks/useRemoteControl.ts | 17 +++-- hooks/useJellyseerr.ts | 2 +- translations/en.json | 70 +++++++++++++++++-- 32 files changed, 200 insertions(+), 90 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index a9a2e2fb..905d02bb 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)/(home)/settings/appearance/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx index 24a3011e..d0109fb7 100644 --- a/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/appearance/hide-libraries/page.tsx @@ -71,7 +71,7 @@ export default function AppearanceHideLibrariesPage() { ))} - {t("home.settings.other.select_liraries_you_want_to_hide")} + {t("home.settings.other.select_libraries_you_want_to_hide")} diff --git a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx index e7a61bde..fe777462 100644 --- a/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/hide-libraries/page.tsx @@ -60,7 +60,7 @@ export default function HideLibrariesPage() { ))} - {t("home.settings.other.select_liraries_you_want_to_hide")} + {t("home.settings.other.select_libraries_you_want_to_hide")} ); diff --git a/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx b/app/(auth)/(tabs)/(libraries)/music/[libraryId]/artists.tsx index f268e0b2..27b03576 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 85b4be5f..db06f727 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 81c2272f..c19d0559 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 934175fb..c1f0dc4b 100644 --- a/app/(auth)/now-playing.tsx +++ b/app/(auth)/now-playing.tsx @@ -6,6 +6,7 @@ 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, @@ -230,7 +231,9 @@ export default function NowPlayingScreen() { paddingBottom: Platform.OS === "android" ? insets.bottom : 0, }} > - No track playing + + {t("music.no_track_playing")} + ); @@ -267,7 +270,7 @@ export default function NowPlayingScreen() { : "text-neutral-500" } > - Now Playing + {t("music.now_playing")} = ({ 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)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 2b269991..4491d1a5 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -1267,7 +1267,7 @@ export default function DirectPlayerPage() { console.error("Video Error:", e.nativeEvent); Alert.alert( t("player.error"), - t("player.an_error_occured_while_playing_the_video"), + t("player.an_error_occurred_while_playing_the_video"), ); writeToLog("ERROR", "Video Error", e.nativeEvent); }} diff --git a/app/(auth)/tv-subtitle-modal.tsx b/app/(auth)/tv-subtitle-modal.tsx index ed5d24d9..92c9c549 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 7fd022ff..c3014be2 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 5487393d..e9f5c397 100644 --- a/components/PlatformDropdown.tsx +++ b/components/PlatformDropdown.tsx @@ -1,5 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; +import { t } from "i18next"; import React, { useEffect } from "react"; import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -380,7 +381,7 @@ const PlatformDropdownComponent = ({ return ( - {trigger || Open Menu} + {trigger || {t("common.open_menu")}} ); }; diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index 462705d6..b9ed11c7 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 c8b6b76e..d6793453 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 c67f6058..da5263c4 100644 --- a/components/downloads/DownloadCard.tsx +++ b/components/downloads/DownloadCard.tsx @@ -173,7 +173,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 e8287132..446bacb2 100644 --- a/components/livetv/TVGuideProgramCell.tsx +++ b/components/livetv/TVGuideProgramCell.tsx @@ -1,4 +1,5 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { t } from "i18next"; import React from "react"; import { Animated, Pressable, StyleSheet, View } from "react-native"; import { Text } from "@/components/common/Text"; @@ -68,7 +69,7 @@ export const TVGuideProgramCell: React.FC = ({ - LIVE + {t("player.live")} )} diff --git a/components/livetv/TVLiveTVPage.tsx b/components/livetv/TVLiveTVPage.tsx index a3f3ed45..4ecfeb80 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/login/TVLogin.tsx b/components/login/TVLogin.tsx index 4ce61192..00bd7f65 100644 --- a/components/login/TVLogin.tsx +++ b/components/login/TVLogin.tsx @@ -437,7 +437,7 @@ export const TVLogin: React.FC = () => { } else { Alert.alert( t("login.connection_failed"), - t("login.an_unexpected_error_occured"), + t("login.an_unexpected_error_occurred"), ); } } finally { @@ -499,7 +499,7 @@ export const TVLogin: React.FC = () => { const message = error instanceof Error ? error.message - : t("login.an_unexpected_error_occured"); + : t("login.an_unexpected_error_occurred"); Alert.alert(t("login.connection_failed"), message); goToQRScreen(); } finally { @@ -523,7 +523,7 @@ export const TVLogin: React.FC = () => { } else { Alert.alert( t("login.connection_failed"), - t("login.an_unexpected_error_occured"), + t("login.an_unexpected_error_occurred"), ); } } finally { @@ -768,7 +768,7 @@ export const TVLogin: React.FC = () => { const message = error instanceof Error ? error.message - : t("login.an_unexpected_error_occured"); + : t("login.an_unexpected_error_occurred"); Alert.alert(t("login.connection_failed"), message); goToQRScreen(); }); diff --git a/components/search/TVSearchTabBadges.tsx b/components/search/TVSearchTabBadges.tsx index d15d43ce..1bac2b01 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 4ef2a800..c481a080 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 && ( - + = ({ ...props }) => { successHapticFeedback(); Alert.alert( t("home.settings.quick_connect.success"), - t("home.settings.quick_connect.quick_connect_autorized"), + t("home.settings.quick_connect.quick_connect_authorized"), ); setQuickConnectCode(undefined); bottomSheetModalRef?.current?.close(); diff --git a/components/tv/TVPosterCard.tsx b/components/tv/TVPosterCard.tsx index fad7261b..1f20e91c 100644 --- a/components/tv/TVPosterCard.tsx +++ b/components/tv/TVPosterCard.tsx @@ -1,6 +1,7 @@ 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 { @@ -371,7 +372,7 @@ export const TVPosterCard: React.FC = ({ fontWeight: "700", }} > - Now Playing + {t("music.now_playing")} ) : null; diff --git a/components/tv/TVSubtitleResultCard.tsx b/components/tv/TVSubtitleResultCard.tsx index f1f3ccf9..405804c6 100644 --- a/components/tv/TVSubtitleResultCard.tsx +++ b/components/tv/TVSubtitleResultCard.tsx @@ -1,5 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import React from "react"; +import { useTranslation } from "react-i18next"; import { ActivityIndicator, Animated, @@ -28,6 +29,7 @@ export const TVSubtitleResultCard = React.forwardRef< const styles = createStyles(typography); const { focused, handleFocus, handleBlur, animatedStyle } = useTVFocusAnimation({ scaleAmount: 1.03 }); + const { t } = useTranslation(); return ( - 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 81c77ab8..ec87b025 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 26f82484..bf813b5c 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")}