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.
This commit is contained in:
Gauvain
2026-06-10 22:29:16 +02:00
parent 0a2dadffd2
commit 0d47c8d43a
32 changed files with 200 additions and 90 deletions

View File

@@ -183,7 +183,7 @@ export const BottomControls: FC<BottomControlsProps> = ({
<SkipButton
showButton={showSkipButton}
onPress={skipIntro}
buttonText='Skip Intro'
buttonText={t("player.skip_intro")}
/>
{/* 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<BottomControlsProps> = ({
showSkipCreditButton && (hasContentAfterCredits || !nextItem)
}
onPress={skipCredit}
buttonText='Skip Credits'
buttonText={t("player.skip_credits")}
/>
{settings.autoPlayNextEpisode !== false &&
(settings.maxAutoPlayEpisodeCount.value === -1 ||

View File

@@ -27,7 +27,7 @@ const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
}
>
<Text className='text-2xl font-bold text-white py-4 '>
Are you still watching ?
{t("player.still_watching")}
</Text>
<Button
onPress={() => {

View File

@@ -4,6 +4,7 @@ import type {
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client";
import { type FC, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Platform, TouchableOpacity, View } from "react-native";
import useRouter from "@/hooks/useAppRouter";
import { useControlsSafeAreaInsets } from "@/hooks/useControlsSafeAreaInsets";
@@ -57,6 +58,7 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
showTechnicalInfo = false,
onToggleTechnicalInfo,
}) => {
const { t } = useTranslation();
const router = useRouter();
const insets = useControlsSafeAreaInsets();
const lightHapticFeedback = useHaptic("light");
@@ -127,8 +129,8 @@ export const HeaderControls: FC<HeaderControlsProps> = ({
onPress={toggleOrientation}
disabled={isTogglingOrientation}
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
accessibilityLabel='Toggle screen orientation'
accessibilityHint='Toggles the screen orientation between portrait and landscape'
accessibilityLabel={t("accessibility.toggle_orientation")}
accessibilityHint={t("accessibility.toggle_orientation_hint")}
>
<MaterialIcons
name='screen-rotation'

View File

@@ -7,6 +7,7 @@ import {
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { Platform, StyleSheet, Text, View } from "react-native";
import Animated, {
Easing,
@@ -184,6 +185,7 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
currentAudioIndex,
}) => {
const typography = useScaledTVTypography();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const safeInsets = useControlsSafeAreaInsets();
const [info, setInfo] = useState<TechnicalInfo | null>(null);
@@ -312,13 +314,13 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
)}
{info?.videoCodec && (
<Text style={textStyle}>
Video: {formatCodec(info.videoCodec)}
{t("player.technical_info.video")} {formatCodec(info.videoCodec)}
{info.fps ? ` @ ${formatFps(info.fps)} fps` : ""}
</Text>
)}
{info?.audioCodec && (
<Text style={textStyle}>
Audio: {formatCodec(info.audioCodec)}
{t("player.technical_info.audio")} {formatCodec(info.audioCodec)}
{streamInfo?.audioChannels
? ` ${formatAudioChannels(streamInfo.audioChannels)}`
: ""}
@@ -326,12 +328,13 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
)}
{streamInfo?.subtitleCodec && (
<Text style={textStyle}>
Subtitle: {formatCodec(streamInfo.subtitleCodec)}
{t("player.technical_info.subtitle")}{" "}
{formatCodec(streamInfo.subtitleCodec)}
</Text>
)}
{(info?.videoBitrate || info?.audioBitrate) && (
<Text style={textStyle}>
Bitrate:{" "}
{t("player.technical_info.bitrate")}{" "}
{info.videoBitrate
? formatBitrate(info.videoBitrate)
: info.audioBitrate
@@ -341,21 +344,26 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
)}
{info?.cacheSeconds !== undefined && (
<Text style={textStyle}>
Buffer: {info.cacheSeconds.toFixed(1)}s
{t("player.technical_info.buffer")} {info.cacheSeconds.toFixed(1)}
s
</Text>
)}
{info?.voDriver && (
<Text style={textStyle}>
VO: {info.voDriver}
{t("player.technical_info.vo")} {info.voDriver}
{info.hwdec ? ` / ${info.hwdec}` : ""}
</Text>
)}
{info?.droppedFrames !== undefined && info.droppedFrames > 0 && (
<Text style={[textStyle, styles.warningText]}>
Dropped: {info.droppedFrames} frames
{t("player.technical_info.dropped_frames", {
count: info.droppedFrames,
})}
</Text>
)}
{!info && !playMethod && <Text style={textStyle}>Loading...</Text>}
{!info && !playMethod && (
<Text style={textStyle}>{t("player.technical_info.loading")}</Text>
)}
</View>
</Animated.View>
);

View File

@@ -1,5 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import {
type OptionGroup,
@@ -54,6 +55,7 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
onRatioChange,
disabled = false,
}) => {
const { t } = useTranslation();
const lightHapticFeedback = useHaptic("light");
const handleRatioSelect = (ratio: AspectRatio) => {
@@ -66,7 +68,10 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
{
options: ASPECT_RATIO_OPTIONS.map((option) => ({
type: "radio" as const,
label: option.label,
label:
option.id === "default"
? t("player.aspect_ratio_original")
: option.label,
value: option.id,
selected: option.id === currentRatio,
onPress: () => handleRatioSelect(option.id),
@@ -94,7 +99,7 @@ export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
return (
<PlatformDropdown
title='Aspect Ratio'
title={t("player.aspect_ratio")}
groups={optionGroups}
trigger={trigger}
bottomSheetConfig={{

View File

@@ -1,6 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import { BITRATES } from "@/components/BitrateSelector";
import {
@@ -47,6 +48,7 @@ const DropdownView = ({
const { settings, updateSettings } = useSettings();
const router = useRouter();
const isOffline = useOfflineMode();
const { t } = useTranslation();
const { subtitleIndex, audioIndex, bitrateValue, playbackPosition } =
useLocalSearchParams<{
@@ -101,7 +103,7 @@ const DropdownView = ({
// Quality Section
if (!isOffline) {
groups.push({
title: "Quality",
title: t("player.menu.quality"),
options:
BITRATES?.map((bitrate) => ({
type: "radio" as const,
@@ -116,7 +118,7 @@ const DropdownView = ({
// Subtitle Section
if (subtitleTracks && subtitleTracks.length > 0) {
groups.push({
title: "Subtitles",
title: t("player.menu.subtitles"),
options: subtitleTracks.map((sub) => ({
type: "radio" as const,
label: sub.name,
@@ -128,7 +130,7 @@ const DropdownView = ({
// Subtitle Scale Section
groups.push({
title: "Subtitle Scale",
title: t("player.menu.subtitle_scale"),
options: SUBTITLE_SCALE_PRESETS.map((preset) => ({
type: "radio" as const,
label: preset.label,
@@ -142,7 +144,7 @@ const DropdownView = ({
// Audio Section
if (audioTracks && audioTracks.length > 0) {
groups.push({
title: "Audio",
title: t("player.menu.audio"),
options: audioTracks.map((track) => ({
type: "radio" as const,
label: track.name,
@@ -156,7 +158,7 @@ const DropdownView = ({
// Speed Section
if (setPlaybackSpeed) {
groups.push({
title: "Speed",
title: t("player.menu.speed"),
options: PLAYBACK_SPEEDS.map((speed) => ({
type: "radio" as const,
label: speed.label,
@@ -174,8 +176,8 @@ const DropdownView = ({
{
type: "action" as const,
label: showTechnicalInfo
? "Hide Technical Info"
: "Show Technical Info",
? t("player.menu.hide_technical_info")
: t("player.menu.show_technical_info"),
onPress: onToggleTechnicalInfo,
},
],
@@ -185,6 +187,7 @@ const DropdownView = ({
return groups;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
t,
isOffline,
bitrateValue,
changeBitrate,
@@ -217,7 +220,7 @@ const DropdownView = ({
return (
<PlatformDropdown
title='Playback Options'
title={t("player.menu.playback_options")}
groups={optionGroups}
trigger={trigger}
expoUIConfig={{}}

View File

@@ -3,6 +3,7 @@ import { Alert } from "react-native";
import { type SharedValue, useSharedValue } from "react-native-reanimated";
import { useTVBackPress } from "@/hooks/useTVBackPress";
import { useTVEventHandler } from "@/hooks/useTVEventHandler";
import i18n from "@/i18n";
interface UseRemoteControlProps {
showControls: boolean;
@@ -124,17 +125,23 @@ export function useRemoteControl({
// Controls are hidden, so confirm before leaving playback.
Alert.alert(
"Stop Playback",
i18n.t("player.stopPlayback"),
videoTitleRef.current
? `Stop playing "${videoTitleRef.current}"?`
: "Are you sure you want to stop playback?",
? i18n.t("player.stopPlayingTitle", {
title: videoTitleRef.current,
})
: i18n.t("player.stopPlayingConfirm"),
[
{
text: "Cancel",
text: i18n.t("common.cancel"),
style: "cancel",
onPress: () => onCancelExitRef.current?.(),
},
{ text: "Stop", style: "destructive", onPress: onBackRef.current },
{
text: i18n.t("common.stop"),
style: "destructive",
onPress: onBackRef.current,
},
],
);
return true;