forked from Ninjalama/streamyfin_mirror
Compare commits
1 Commits
feat/hide-
...
feat/i18n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
752cb1cdc6 |
5
app.json
5
app.json
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"android": {
|
||||
"jsEngine": "hermes",
|
||||
"versionCode": 17,
|
||||
"versionCode": 18,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/icon.png"
|
||||
},
|
||||
@@ -96,7 +96,8 @@
|
||||
{
|
||||
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-localization"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { router, Tabs } from "expo-router";
|
||||
import React, { useEffect } from "react";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { Platform, TouchableOpacity, View } from "react-native";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { StyleSheet } from "react-native";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { Tabs } from "expo-router";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, StyleSheet } from "react-native";
|
||||
|
||||
export default function TabLayout() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "android") {
|
||||
NavigationBar.setBackgroundColorAsync("#121212");
|
||||
@@ -53,7 +53,7 @@ export default function TabLayout() {
|
||||
name="home"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "Home",
|
||||
title: t("tabs.home"),
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<TabBarIcon
|
||||
name={focused ? "home" : "home-outline"}
|
||||
@@ -66,7 +66,7 @@ export default function TabLayout() {
|
||||
name="search"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "Search",
|
||||
title: t("tabs.search"),
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<TabBarIcon name={focused ? "search" : "search"} color={color} />
|
||||
),
|
||||
@@ -76,7 +76,7 @@ export default function TabLayout() {
|
||||
name="library"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "Library",
|
||||
title: t("tabs.library"),
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<TabBarIcon
|
||||
name={focused ? "apps" : "apps-outline"}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Chromecast } from "@/components/Chromecast";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
|
||||
export default function IndexLayout() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
@@ -13,7 +15,7 @@ export default function IndexLayout() {
|
||||
options={{
|
||||
headerShown: true,
|
||||
headerLargeTitle: true,
|
||||
headerTitle: "Home",
|
||||
headerTitle: t("home.home"),
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerShadowVisible: false,
|
||||
|
||||
@@ -20,12 +20,15 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RefreshControl, ScrollView, View } from "react-native";
|
||||
|
||||
export default function index() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
@@ -216,9 +219,9 @@ export default function index() {
|
||||
if (isConnected === false) {
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
||||
<Text className="text-3xl font-bold mb-2">No Internet</Text>
|
||||
<Text className="text-3xl font-bold mb-2">{t("home.noInternet")}</Text>
|
||||
<Text className="text-center opacity-70">
|
||||
No worries, you can still watch{"\n"}downloaded content.
|
||||
{t("home.noInternetMessage")}
|
||||
</Text>
|
||||
<View className="mt-4">
|
||||
<Button
|
||||
@@ -229,7 +232,7 @@ export default function index() {
|
||||
<Ionicons name="arrow-forward" size={20} color="white" />
|
||||
}
|
||||
>
|
||||
Go to downloads
|
||||
{t("home.goToDownloads")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
@@ -239,10 +242,8 @@ export default function index() {
|
||||
if (isError)
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
||||
<Text className="text-3xl font-bold mb-2">Oops!</Text>
|
||||
<Text className="text-center opacity-70">
|
||||
Something went wrong.{"\n"}Please log out and in again.
|
||||
</Text>
|
||||
<Text className="text-3xl font-bold mb-2">{t("home.oops")}</Text>
|
||||
<Text className="text-center opacity-70">{t("home.errorMessage")}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -265,14 +266,14 @@ export default function index() {
|
||||
<LargeMovieCarousel />
|
||||
|
||||
<ScrollingCollectionList
|
||||
title="Continue Watching"
|
||||
title={t("home.continueWatching")}
|
||||
data={data}
|
||||
loading={isLoading}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
|
||||
<ScrollingCollectionList
|
||||
title="Next Up"
|
||||
title={t("home.nextUp")}
|
||||
data={nextUpData}
|
||||
loading={isLoadingNextUp}
|
||||
orientation="horizontal"
|
||||
@@ -283,19 +284,19 @@ export default function index() {
|
||||
))}
|
||||
|
||||
<ScrollingCollectionList
|
||||
title="Recently Added in Movies"
|
||||
title={t("home.recentlyAddedMovies")}
|
||||
data={recentlyAddedInMovies}
|
||||
loading={isLoadingRecentlyAddedMovies}
|
||||
/>
|
||||
|
||||
<ScrollingCollectionList
|
||||
title="Recently Added in TV-Shows"
|
||||
title={t("home.recentlyAddedTVShows")}
|
||||
data={recentlyAddedInTVShows}
|
||||
loading={isLoadingRecentlyAddedTVShows}
|
||||
/>
|
||||
|
||||
<ScrollingCollectionList
|
||||
title="Suggestions"
|
||||
title={t("home.suggestions")}
|
||||
data={suggestions}
|
||||
loading={isLoadingSuggestions}
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -17,6 +17,9 @@ import { useKeepAwake } from "expo-keep-awake";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import { I18nextProvider, useTranslation } from "react-i18next";
|
||||
import i18n from "@/i18n";
|
||||
import { getLocales } from "expo-localization";
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
@@ -42,7 +45,9 @@ export default function RootLayout() {
|
||||
|
||||
return (
|
||||
<JotaiProvider>
|
||||
<Layout />
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Layout />
|
||||
</I18nextProvider>
|
||||
</JotaiProvider>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +57,8 @@ function Layout() {
|
||||
|
||||
useKeepAwake();
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const queryClientRef = useRef<QueryClient>(
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -75,6 +82,12 @@ function Layout() {
|
||||
);
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(
|
||||
settings?.preferedLanguage || getLocales()[0].languageCode || "en"
|
||||
);
|
||||
}, [settings]);
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<QueryClientProvider client={queryClientRef.current}>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||
import { apiAtom, useJellyfin } from "@/providers/JellyfinProvider";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { AxiosError } from "axios";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
@@ -21,6 +23,7 @@ const CredentialsSchema = z.object({
|
||||
});
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { setServer, login, removeServer } = useJellyfin();
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
@@ -72,7 +75,7 @@ const Login: React.FC = () => {
|
||||
<View className="mb-4">
|
||||
<Text className="text-3xl font-bold mb-2">Streamyfin</Text>
|
||||
<Text className="text-neutral-500 mb-2">
|
||||
Server: {api.basePath}
|
||||
{t("server.server_label", { serverURL: api.basePath })}
|
||||
</Text>
|
||||
<Button
|
||||
color="black"
|
||||
@@ -89,17 +92,17 @@ const Login: React.FC = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
Change server
|
||||
{t("server.change_server")}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-col space-y-2">
|
||||
<Text className="text-2xl font-bold">Log in</Text>
|
||||
<Text className="text-2xl font-bold">{t("login.login")}</Text>
|
||||
<Text className="text-neutral-500">
|
||||
Log in to any user account
|
||||
{t("login.login_subtitle")}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder="Username"
|
||||
placeholder={t("login.username_placeholder")}
|
||||
onChangeText={(text) =>
|
||||
setCredentials({ ...credentials, username: text })
|
||||
}
|
||||
@@ -116,7 +119,7 @@ const Login: React.FC = () => {
|
||||
|
||||
<Input
|
||||
className="mb-2"
|
||||
placeholder="Password"
|
||||
placeholder={t("login.password_placeholder")}
|
||||
onChangeText={(text) =>
|
||||
setCredentials({ ...credentials, password: text })
|
||||
}
|
||||
@@ -139,7 +142,7 @@ const Login: React.FC = () => {
|
||||
loading={loading}
|
||||
className="mt-auto mb-2"
|
||||
>
|
||||
Log in
|
||||
{t("login.login_button")}
|
||||
</Button>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
@@ -158,10 +161,10 @@ const Login: React.FC = () => {
|
||||
<View className="flex flex-col gap-y-2">
|
||||
<Text className="text-3xl font-bold">Streamyfin</Text>
|
||||
<Text className="text-neutral-500">
|
||||
Connect to your Jellyfin server
|
||||
{t("server.connect_to_server")}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder="Server URL"
|
||||
placeholder={t("server.server_url_placeholder")}
|
||||
onChangeText={setServerURL}
|
||||
value={serverURL}
|
||||
keyboardType="url"
|
||||
@@ -170,12 +173,11 @@ const Login: React.FC = () => {
|
||||
textContentType="URL"
|
||||
maxLength={500}
|
||||
/>
|
||||
<Text className="opacity-30">
|
||||
Server URL requires http or https
|
||||
</Text>
|
||||
<Text className="opacity-30">{t("server.server_url_hint")}</Text>
|
||||
<LanguageSwitcher />
|
||||
</View>
|
||||
<Button onPress={() => handleConnect(serverURL)} className="mb-2">
|
||||
Connect
|
||||
{t("server.connect_button")}
|
||||
</Button>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
36
components/LanguageSwitcher.tsx
Normal file
36
components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getLocales } from "expo-localization";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
|
||||
interface Props extends ViewProps {}
|
||||
|
||||
export const LanguageSwitcher: React.FC<Props> = ({ ...props }) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const lngs = ["en", "sv"];
|
||||
|
||||
const [settings, updateSettings] = useSettings();
|
||||
return (
|
||||
<View className="flex flex-row space-x-2" {...props}>
|
||||
{lngs.map((l) => (
|
||||
<TouchableOpacity
|
||||
key={l}
|
||||
onPress={() => {
|
||||
i18n.changeLanguage(l);
|
||||
updateSettings({ preferedLanguage: l });
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`uppercase ${
|
||||
i18n.language === l ? "text-blue-500" : "text-gray-400 underline"
|
||||
}`}
|
||||
>
|
||||
{l}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
22
i18n.ts
Normal file
22
i18n.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import en from "./translations/en.json";
|
||||
import sv from "./translations/sv.json";
|
||||
import { getLocales } from "expo-localization";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
compatibilityJSON: "v3",
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
sv: { translation: sv },
|
||||
},
|
||||
|
||||
lng: getLocales()[0].languageCode || "en",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -41,6 +41,7 @@
|
||||
"expo-image": "~1.12.13",
|
||||
"expo-keep-awake": "~13.0.2",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-localization": "~15.0.3",
|
||||
"expo-navigation-bar": "~3.0.7",
|
||||
"expo-router": "~3.5.23",
|
||||
"expo-screen-orientation": "~7.0.5",
|
||||
@@ -51,11 +52,13 @@
|
||||
"expo-updates": "~0.25.22",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"ffmpeg-kit-react-native": "^6.0.2",
|
||||
"i18next": "^23.13.0",
|
||||
"jotai": "^2.9.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nativewind": "^2.0.11",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-native": "0.74.5",
|
||||
"react-native-circular-progress": "^1.4.0",
|
||||
"react-native-compressor": "^1.8.25",
|
||||
|
||||
38
translations/en.json
Normal file
38
translations/en.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"login": {
|
||||
"username_required": "Username is required",
|
||||
"error_title": "Error",
|
||||
"url_error_message": "URL needs to start with http or https.",
|
||||
"login": "Log in",
|
||||
"login_subtitle": "Log in to any user account",
|
||||
"username_placeholder": "Username",
|
||||
"password_placeholder": "Password",
|
||||
"login_button": "Log in"
|
||||
},
|
||||
"server": {
|
||||
"server_label": "Server: {{serverURL}}",
|
||||
"change_server": "Change server",
|
||||
"connect_to_server": "Connect to your Jellyfin server",
|
||||
"server_url_placeholder": "Server URL",
|
||||
"server_url_hint": "Server URL requires http or https",
|
||||
"connect_button": "Connect"
|
||||
},
|
||||
"home": {
|
||||
"home": "Home",
|
||||
"noInternet": "No Internet",
|
||||
"noInternetMessage": "No worries, you can still watch\ndownloaded content.",
|
||||
"goToDownloads": "Go to downloads",
|
||||
"oops": "Oops!",
|
||||
"errorMessage": "Something went wrong.\nPlease log out and in again.",
|
||||
"continueWatching": "Continue Watching",
|
||||
"nextUp": "Next Up",
|
||||
"recentlyAddedMovies": "Recently Added in Movies",
|
||||
"recentlyAddedTVShows": "Recently Added in TV-Shows",
|
||||
"suggestions": "Suggestions"
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Home",
|
||||
"search": "Search",
|
||||
"library": "Library"
|
||||
}
|
||||
}
|
||||
38
translations/sv.json
Normal file
38
translations/sv.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"login": {
|
||||
"username_required": "Användarnamn krävs",
|
||||
"error_title": "Fel",
|
||||
"url_error_message": "URL måste börja med http eller https.",
|
||||
"login_title": "Logga in",
|
||||
"login_subtitle": "Logga in på ett användarkonto",
|
||||
"username_placeholder": "Användarnamn",
|
||||
"password_placeholder": "Lösenord",
|
||||
"login_button": "Logga in"
|
||||
},
|
||||
"server": {
|
||||
"server_label": "Server: {{serverURL}}",
|
||||
"change_server": "Byt server",
|
||||
"connect_to_server": "Anslut till din Jellyfin-server",
|
||||
"server_url_placeholder": "Server URL",
|
||||
"server_url_hint": "Server URL kräver http eller https",
|
||||
"connect_button": "Anslut"
|
||||
},
|
||||
"home": {
|
||||
"home": "Hem",
|
||||
"noInternet": "Ingen Internet",
|
||||
"noInternetMessage": "Ingen fara, du kan fortfarande titta\npå nedladdat innehåll.",
|
||||
"goToDownloads": "Gå till nedladdningar",
|
||||
"oops": "Hoppsan!",
|
||||
"errorMessage": "Något gick fel.\nLogga ut och in igen.",
|
||||
"continueWatching": "Fortsätt titta",
|
||||
"nextUp": "Nästa upp",
|
||||
"recentlyAddedMovies": "Nyligen tillagt i Filmer",
|
||||
"recentlyAddedTVShows": "Nyligen tillagt i TV-Serier",
|
||||
"suggestions": "Förslag"
|
||||
},
|
||||
"tabs": {
|
||||
"home": "Hem",
|
||||
"search": "Sök",
|
||||
"library": "Bibliotek"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useEffect } from "react";
|
||||
import { getLocales } from "expo-localization";
|
||||
|
||||
type Settings = {
|
||||
autoRotate?: boolean;
|
||||
@@ -10,6 +11,7 @@ type Settings = {
|
||||
deviceProfile?: "Expo" | "Native" | "Old";
|
||||
forceDirectPlay?: boolean;
|
||||
mediaListCollectionIds?: string[];
|
||||
preferedLanguage?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,7 @@ const loadSettings = async (): Promise<Settings> => {
|
||||
deviceProfile: "Expo",
|
||||
forceDirectPlay: false,
|
||||
mediaListCollectionIds: [],
|
||||
preferedLanguage: getLocales()[0] || "en",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user