mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-12 17:00:23 +01:00
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:
@@ -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 ||
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{}}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user