mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
feat: enable manually setting language in settings
This commit is contained in:
@@ -273,9 +273,9 @@ function Layout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(
|
i18n.changeLanguage(
|
||||||
settings?.preferedLanguage || getLocales()[0].languageCode || "en"
|
settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en"
|
||||||
);
|
);
|
||||||
}, [settings]);
|
}, [settings?.preferedLanguage, i18n]);
|
||||||
|
|
||||||
const appState = useRef(AppState.currentState);
|
const appState = useRef(AppState.currentState);
|
||||||
|
|
||||||
|
|||||||
80
components/settings/AppLanguageSelector.tsx
Normal file
80
components/settings/AppLanguageSelector.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
|
import { TouchableOpacity, View } from "react-native";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { APP_LANGUAGES } from "@/i18n";
|
||||||
|
|
||||||
|
export const AppLanguageSelector = () => {
|
||||||
|
const [settings, updateSettings] = useSettings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="mb-4">
|
||||||
|
<Text className="text-lg font-bold mb-2">
|
||||||
|
{t("home.settings.languages.title")}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
className={`
|
||||||
|
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4 rounded-xl
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<View className="flex flex-col shrink">
|
||||||
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.languages.app_language")}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs opacity-50">
|
||||||
|
{t("home.settings.languages.app_language_description")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||||
|
<Text>
|
||||||
|
{APP_LANGUAGES.find(
|
||||||
|
(l) => l.value === settings?.preferedLanguage
|
||||||
|
)?.label || t("home.settings.languages.system")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
loop={true}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
alignOffset={0}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={8}
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{t("home.settings.languages.title")}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={"unknown"}
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
preferedLanguage: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>
|
||||||
|
{t("home.settings.languages.system")}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{APP_LANGUAGES?.map((l) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={l?.value ?? "unknown"}
|
||||||
|
onSelect={() => {
|
||||||
|
updateSettings({
|
||||||
|
preferedLanguage: l.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>{l.label}</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -44,6 +44,7 @@ import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
|||||||
import { ListItem } from "@/components/ListItem";
|
import { ListItem } from "@/components/ListItem";
|
||||||
import { JellyseerrSettings } from "./Jellyseerr";
|
import { JellyseerrSettings } from "./Jellyseerr";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AppLanguageSelector } from "./AppLanguageSelector";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -133,6 +134,8 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</View>
|
</View>
|
||||||
</View> */}
|
</View> */}
|
||||||
|
|
||||||
|
<AppLanguageSelector />
|
||||||
|
|
||||||
<MediaProvider>
|
<MediaProvider>
|
||||||
<MediaToggles />
|
<MediaToggles />
|
||||||
<AudioToggles />
|
<AudioToggles />
|
||||||
@@ -140,12 +143,16 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</MediaProvider>
|
</MediaProvider>
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
<Text className="text-lg font-bold mb-2">{t("home.settings.other.other_title")}</Text>
|
<Text className="text-lg font-bold mb-2">
|
||||||
|
{t("home.settings.other.other_title")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<View className="flex flex-col rounded-xl overflow-hidden divide-y-2 divide-solid divide-neutral-800">
|
<View className="flex flex-col rounded-xl overflow-hidden divide-y-2 divide-solid divide-neutral-800">
|
||||||
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
||||||
<View className="shrink">
|
<View className="shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.other.auto_rotate")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.other.auto_rotate")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{t("home.settings.other.auto_rotate_hint")}
|
{t("home.settings.other.auto_rotate_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -168,7 +175,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.other.video_orientation")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.other.video_orientation")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{t("home.settings.other.video_orientation_hint")}
|
{t("home.settings.other.video_orientation_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -265,7 +274,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
|
|
||||||
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
||||||
<View className="shrink">
|
<View className="shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.other.safe_area_in_controls")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.other.safe_area_in_controls")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{t("home.settings.other.safe_area_in_controls_hint")}
|
{t("home.settings.other.safe_area_in_controls_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -281,8 +292,12 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<Text className="font-semibold">{t("home.settings.other.use_popular_lists_plugin")}</Text>
|
<Text className="font-semibold">
|
||||||
<Text className="text-xs opacity-50">{t("home.settings.other.use_popular_lists_plugin_hint")}</Text>
|
{t("home.settings.other.use_popular_lists_plugin")}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-xs opacity-50">
|
||||||
|
{t("home.settings.other.use_popular_lists_plugin_hint")}
|
||||||
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Linking.openURL(
|
Linking.openURL(
|
||||||
@@ -290,7 +305,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="text-xs text-purple-600">{t("home.settings.other.more_info")}</Text>
|
<Text className="text-xs text-purple-600">
|
||||||
|
{t("home.settings.other.more_info")}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -355,7 +372,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.other.search_engine")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.other.search_engine")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{t("home.settings.other.search_engine_hint")}
|
{t("home.settings.other.search_engine_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -438,7 +457,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
|
|
||||||
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
||||||
<View className="shrink">
|
<View className="shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.other.show_custom_menu_links")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.other.show_custom_menu_links")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{t("home.settings.other.show_custom_menu_links_hint")}
|
{t("home.settings.other.show_custom_menu_links_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -449,7 +470,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text className="text-xs text-purple-600">{t("home.settings.other.more_info")}</Text>
|
<Text className="text-xs text-purple-600">
|
||||||
|
{t("home.settings.other.more_info")}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -463,7 +486,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="mt-4">
|
<View className="mt-4">
|
||||||
<Text className="text-lg font-bold mb-2">{t("home.settings.downloads.downloads_title")}</Text>
|
<Text className="text-lg font-bold mb-2">
|
||||||
|
{t("home.settings.downloads.downloads_title")}
|
||||||
|
</Text>
|
||||||
<View className="flex flex-col rounded-xl overflow-hidden divide-y-2 divide-solid divide-neutral-800">
|
<View className="flex flex-col rounded-xl overflow-hidden divide-y-2 divide-solid divide-neutral-800">
|
||||||
<View
|
<View
|
||||||
className={`
|
className={`
|
||||||
@@ -471,7 +496,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.downloads.download_method")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.downloads.download_method")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
{t("home.settings.downloads.download_method_hint")}
|
{t("home.settings.downloads.download_method_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -531,7 +558,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.downloads.remux_max_download")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.downloads.remux_max_download")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50 shrink">
|
<Text className="text-xs opacity-50 shrink">
|
||||||
{t("home.settings.downloads.remux_max_download_hint")}
|
{t("home.settings.downloads.remux_max_download_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -562,7 +591,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<View className="flex flex-col shrink">
|
<View className="flex flex-col shrink">
|
||||||
<Text className="font-semibold">{t("home.settings.downloads.auto_download")}</Text>
|
<Text className="font-semibold">
|
||||||
|
{t("home.settings.downloads.auto_download")}
|
||||||
|
</Text>
|
||||||
<Text className="text-xs opacity-50 shrink">
|
<Text className="text-xs opacity-50 shrink">
|
||||||
{t("home.settings.downloads.auto_download_hint")}
|
{t("home.settings.downloads.auto_download_hint")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
8
i18n.ts
8
i18n.ts
@@ -6,8 +6,14 @@ import fr from "./translations/fr.json";
|
|||||||
import sv from "./translations/sv.json";
|
import sv from "./translations/sv.json";
|
||||||
import { getLocales } from "expo-localization";
|
import { getLocales } from "expo-localization";
|
||||||
|
|
||||||
|
export const APP_LANGUAGES = [
|
||||||
|
{ label: "English", value: "en" },
|
||||||
|
{ label: "Français", value: "fr" },
|
||||||
|
{ label: "Svenska", value: "sv" },
|
||||||
|
];
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
compatibilityJSON: "v3",
|
compatibilityJSON: "v4",
|
||||||
resources: {
|
resources: {
|
||||||
en: { translation: en },
|
en: { translation: en },
|
||||||
fr: { translation: fr },
|
fr: { translation: fr },
|
||||||
|
|||||||
@@ -33,13 +33,14 @@
|
|||||||
"user_info_title": "User Info",
|
"user_info_title": "User Info",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"server": "Server",
|
"server": "Server",
|
||||||
"log_out_button": "Log out"
|
"log_out_button": "Log out",
|
||||||
|
"token": "Token"
|
||||||
},
|
},
|
||||||
"quick_connect": {
|
"quick_connect": {
|
||||||
"quick_connect_title": "Quick connect",
|
"quick_connect_title": "Quick connect",
|
||||||
"authorize_button": "Authorize"
|
"authorize_button": "Authorize"
|
||||||
},
|
},
|
||||||
"media":{
|
"media": {
|
||||||
"media_title": "Media",
|
"media_title": "Media",
|
||||||
"forward_skip_length": "Forward skip length",
|
"forward_skip_length": "Forward skip length",
|
||||||
"forward_skip_length_hint": "Choose length in seconds when skipping in video playback.",
|
"forward_skip_length_hint": "Choose length in seconds when skipping in video playback.",
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
"subtitle_mode": "Subtitle Mode",
|
"subtitle_mode": "Subtitle Mode",
|
||||||
"subtitle_mode_hint": "Subtitles are loaded based on the default and forced flags in the\nembedded metadata. Language preferences are considered when\nmultiple options are available.",
|
"subtitle_mode_hint": "Subtitles are loaded based on the default and forced flags in the\nembedded metadata. Language preferences are considered when\nmultiple options are available.",
|
||||||
"set_subtitle_track": "Set Subtitle Track From Previous Item",
|
"set_subtitle_track": "Set Subtitle Track From Previous Item",
|
||||||
"set_subtitle_track_hint" :"Try to set the subtitle track to the closest match to the last\nvideo.",
|
"set_subtitle_track_hint": "Try to set the subtitle track to the closest match to the last\nvideo.",
|
||||||
"subtitle_size": "Subtitle Size",
|
"subtitle_size": "Subtitle Size",
|
||||||
"subtitle_size_hint": "Choose a default subtitle size for direct play (only works for\nsome subtitle formats)."
|
"subtitle_size_hint": "Choose a default subtitle size for direct play (only works for\nsome subtitle formats)."
|
||||||
},
|
},
|
||||||
@@ -116,6 +117,12 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"logs_title": "Logs",
|
"logs_title": "Logs",
|
||||||
"no_logs_available": "No logs available"
|
"no_logs_available": "No logs available"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"title": "Languages",
|
||||||
|
"app_language": "App language",
|
||||||
|
"app_language_description": "Select the language for the app.",
|
||||||
|
"system": "System"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ const loadSettings = (): Settings => {
|
|||||||
usePopularPlugin: false,
|
usePopularPlugin: false,
|
||||||
deviceProfile: "Expo",
|
deviceProfile: "Expo",
|
||||||
mediaListCollectionIds: [],
|
mediaListCollectionIds: [],
|
||||||
preferedLanguage: getLocales()[0].languageCode || "en",
|
preferedLanguage: undefined,
|
||||||
searchEngine: "Jellyfin",
|
searchEngine: "Jellyfin",
|
||||||
marlinServerUrl: "",
|
marlinServerUrl: "",
|
||||||
openInVLC: false,
|
openInVLC: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user