Skip to content

Commit bcb68cc

Browse files
Merge pull request #105 from mykhailodanilenko/feature/add-i18n
Add i18n
2 parents 9b3a50b + 88a7792 commit bcb68cc

36 files changed

+610
-99
lines changed

OwnTube.tv/api/instanceSearchApi.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios, { AxiosInstance } from "axios";
22
import { PeertubeInstance } from "./models";
3+
import i18n from "../i18n";
34

45
export class InstanceSearchApi {
56
private instance!: AxiosInstance;
@@ -37,7 +38,7 @@ export class InstanceSearchApi {
3738
});
3839
return response.data;
3940
} catch (error: unknown) {
40-
throw new Error(`Failed to fetch instances: ${(error as Error).message}`);
41+
throw new Error(i18n.t("errors.failedToFetchInstances", { error: (error as Error).message }));
4142
}
4243
}
4344
}

OwnTube.tv/api/peertubeVideosApi.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import axios, { AxiosInstance } from "axios";
22
import { VideosCommonQuery } from "@peertube/peertube-types";
33
import { Video } from "@peertube/peertube-types/peertube-models/videos/video.model";
44
import { GetVideosVideo } from "./models";
5+
import i18n from "../i18n";
56

67
/**
78
* Get videos from the PeerTube backend `/api/v1/videos` API
@@ -20,7 +21,7 @@ export class PeertubeVideosApi {
2021
private _maxChunkSize!: number;
2122
set maxChunkSize(value: number) {
2223
if (!(value > 0 && value <= 100)) {
23-
throw new Error("The maximum number of videos to fetch in a single request is 100");
24+
throw new Error("errors.maxVideosPerChunk");
2425
}
2526
this._maxChunkSize = value;
2627
}
@@ -70,7 +71,7 @@ export class PeertubeVideosApi {
7071
});
7172
return response.data.total as number;
7273
} catch (error: unknown) {
73-
throw new Error(`Failed to fetch total number of videos from PeerTube API: ${(error as Error).message}`);
74+
throw new Error(i18n.t("errors.failedToFetchTotalVids", { error: (error as Error).message }));
7475
}
7576
}
7677

@@ -92,7 +93,7 @@ export class PeertubeVideosApi {
9293
})
9394
).data.data as Required<GetVideosVideo>[];
9495
} catch (error: unknown) {
95-
throw new Error(`Failed to fetch videos from PeerTube API: ${(error as Error).message}`);
96+
throw new Error(i18n.t("errors.failedToFetchVids", { error: (error as Error).message }));
9697
}
9798
} else {
9899
let rawTotal = -1;
@@ -104,17 +105,15 @@ export class PeertubeVideosApi {
104105
fetchCount = rawTotal - offset;
105106
if (this.debugLogging) {
106107
console.debug(
107-
`We would exceed the total available ${rawTotal} videos with chunk size ${this.maxChunkSize}, so fetching only ${fetchCount} videos to reach the total`,
108+
i18n.t("errors.maxTotalToBeExceeded", { rawTotal, maxChunkSize: this.maxChunkSize, fetchCount }),
108109
);
109110
}
110111
}
111112
const maxLimitToBeExceeded = rawVideos.length + fetchCount > limit;
112113
if (maxLimitToBeExceeded) {
113114
fetchCount = limit - offset;
114115
if (this.debugLogging) {
115-
console.debug(
116-
`We would exceed max limit of ${limit} videos, so fetching only ${fetchCount} additional videos to reach the limit`,
117-
);
116+
console.debug(i18n.t("errors.maxLimitToBeExceeded", { limit, fetchCount }));
118117
}
119118
}
120119
try {
@@ -162,7 +161,7 @@ export class PeertubeVideosApi {
162161

163162
return response.data;
164163
} catch (error: unknown) {
165-
throw new Error(`Failed to fetch videos from PeerTube API: ${(error as Error).message}`);
164+
throw new Error(i18n.t("errors.failedToFetchVideo", { error: (error as Error).message, id }));
166165
}
167166
}
168167
}

OwnTube.tv/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@
3030
"bundler": "metro",
3131
"favicon": "assets/faviconLight-48x48.png"
3232
},
33-
"plugins": ["expo-router", "expo-font"]
33+
"plugins": ["expo-router", "expo-font", "expo-localization"]
3434
}
3535
}

OwnTube.tv/app/(home)/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useTheme } from "@react-navigation/native";
1010
import { StyleSheet, View } from "react-native";
1111
import { useRecentInstances } from "../../hooks";
1212
import { RootStackParams } from "../_layout";
13+
import { useTranslation } from "react-i18next";
1314

1415
export default function index() {
1516
const router = useRouter();
@@ -18,6 +19,7 @@ export default function index() {
1819
const { backend } = useLocalSearchParams<RootStackParams[ROUTES.INDEX]>();
1920
const [isGettingStoredBackend, setIsGettingStoredBackend] = useState(true);
2021
const { recentInstances, addRecentInstance } = useRecentInstances();
22+
const { t } = useTranslation();
2123

2224
const getSourceAndRedirect = async () => {
2325
if (backend) {
@@ -39,7 +41,7 @@ export default function index() {
3941
useCallback(() => {
4042
if (backend) {
4143
navigation.setOptions({
42-
title: `OwnTube.tv@${backend}`,
44+
title: `${t("appName")}@${backend}`,
4345
headerRight: () => (
4446
<View style={styles.headerControls}>
4547
<Link
@@ -69,7 +71,9 @@ export default function index() {
6971
return (
7072
<>
7173
<Head>
72-
<title>OwnTube.tv@{backend || ""}</title>
74+
<title>
75+
{t("appName")}@{backend || ""}
76+
</title>
7377
<meta name="description" content="OwnTube.tv homepage" />
7478
</Head>
7579
{!!backend && <HomeScreen />}

OwnTube.tv/app/(home)/settings.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { SettingsScreen } from "../../screens";
22
import Head from "expo-router/head";
3+
import { useTranslation } from "react-i18next";
34

45
export default function settings() {
6+
const { t } = useTranslation();
7+
58
return (
69
<>
710
<Head>
8-
<title>Settings</title>
9-
<meta name="description" content="OwnTube.tv settings" />
11+
<title>{t("settingsPageTitle")}</title>
12+
<meta name="description" content={`${t("appName")} ${t("settingsPageTitle").toLowerCase()}`} />
1013
</Head>
1114
<SettingsScreen />
1215
</>

OwnTube.tv/app/(home)/video.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ import Head from "expo-router/head";
33
import { useGetVideoQuery } from "../../api";
44
import { useLocalSearchParams } from "expo-router";
55
import { RootStackParams } from "../_layout";
6+
import { useTranslation } from "react-i18next";
67

78
export default function video() {
89
const { id } = useLocalSearchParams<RootStackParams["video"]>();
910
const { data: title } = useGetVideoQuery(id, (data) => data.name);
11+
const { t } = useTranslation();
1012

1113
return (
1214
<>
1315
<Head>
1416
<title>{title}</title>
15-
<meta name="description" content="View video" />
17+
<meta name="description" content={t("viewVideo")} />
1618
</Head>
1719
<VideoScreen />
1820
</>

OwnTube.tv/app/_layout.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Link, Stack, useLocalSearchParams } from "expo-router";
22
import { Platform, StyleSheet } from "react-native";
3-
import { ROUTES } from "../types";
3+
import { ROUTES, STORAGE } from "../types";
44
import { Ionicons } from "@expo/vector-icons";
55
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
66
import { AppConfigContextProvider, ColorSchemeContextProvider, useColorSchemeContext } from "../contexts";
@@ -9,11 +9,20 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
99
import { useFonts } from "expo-font";
1010
import Toast from "react-native-toast-message";
1111
import { BuildInfoToast, ClickableHeaderText } from "../components";
12+
import "../i18n";
13+
import { useTranslation } from "react-i18next";
14+
import { useEffect } from "react";
15+
import { readFromAsyncStorage } from "../utils";
1216

1317
const RootStack = () => {
1418
const { backend } = useLocalSearchParams();
1519
const { scheme } = useColorSchemeContext();
1620
const theme = scheme === "dark" ? DarkTheme : DefaultTheme;
21+
const { t, i18n } = useTranslation();
22+
23+
useEffect(() => {
24+
readFromAsyncStorage(STORAGE.LOCALE).then(i18n.changeLanguage);
25+
}, []);
1726

1827
return (
1928
<ThemeProvider value={theme}>
@@ -26,14 +35,14 @@ const RootStack = () => {
2635
name={"(home)/index"}
2736
options={{
2837
headerBackVisible: false,
29-
title: "OwnTube.tv",
38+
title: t("appName"),
3039
headerLeft: () => <></>,
3140
headerRight: () => <></>,
3241
}}
3342
/>
3443
<Stack.Screen
3544
options={{
36-
title: "Settings",
45+
title: t("settingsPageTitle"),
3746
headerBackVisible: false,
3847
headerLeft: () => (
3948
<Link style={styles.headerButtonLeft} href={{ pathname: "/", params: { backend } }}>
@@ -43,7 +52,7 @@ const RootStack = () => {
4352
}}
4453
name={`(home)/${ROUTES.SETTINGS}`}
4554
/>
46-
<Stack.Screen options={{ title: "Video", headerShown: false }} name={`(home)/video`} />
55+
<Stack.Screen options={{ title: t("videoPageTitle"), headerShown: false }} name={`(home)/video`} />
4756
</Stack>
4857
<Toast config={{ buildInfo: () => <BuildInfoToast /> }} />
4958
</ThemeProvider>

OwnTube.tv/components/AppConfig.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,27 @@ import { DeviceCapabilities } from "./DeviceCapabilities";
22
import { StyleSheet, Switch, View } from "react-native";
33
import { Typography } from "./Typography";
44
import { useAppConfigContext, useColorSchemeContext } from "../contexts";
5+
import { useTranslation } from "react-i18next";
6+
import { SelectLanguage } from "./SelectLanguage";
57

68
export const AppConfig = () => {
79
const { isDebugMode, setIsDebugMode } = useAppConfigContext();
810
const { scheme, toggleScheme } = useColorSchemeContext();
11+
const { t } = useTranslation();
912

1013
return (
1114
<View style={styles.deviceInfoAndToggles}>
1215
<DeviceCapabilities />
1316
<View style={styles.togglesContainer}>
1417
<View style={styles.option}>
15-
<Typography>Debug logging</Typography>
18+
<Typography>{t("debugLogging")}</Typography>
1619
<Switch value={isDebugMode} onValueChange={setIsDebugMode} />
1720
</View>
1821
<View style={styles.option}>
19-
<Typography>Toggle Theme</Typography>
22+
<Typography>{t("toggleTheme")}</Typography>
2023
<Switch value={scheme === "light"} onValueChange={toggleScheme} />
2124
</View>
25+
<SelectLanguage />
2226
</View>
2327
</View>
2428
);

OwnTube.tv/components/BuildInfoToast.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@ import build_info from "../build-info.json";
44
import { useTheme } from "@react-navigation/native";
55
import { removeSecondsFromISODate } from "../utils";
66
import { ExternalLink } from "./ExternalLink";
7+
import { useTranslation } from "react-i18next";
78

89
export const BuildInfoToast = () => {
910
const { colors } = useTheme();
11+
const { t } = useTranslation();
1012

1113
return (
1214
<View style={[{ backgroundColor: colors.card }, styles.container]}>
1315
<Typography fontSize={14} style={{ userSelect: "none" }}>
14-
Revision{" "}
16+
{t("revision")}{" "}
1517
<ExternalLink absoluteHref={build_info.COMMIT_URL}>
1618
<Typography fontSize={14} style={styles.link}>
1719
{build_info.GITHUB_SHA_SHORT}
1820
</Typography>{" "}
1921
</ExternalLink>
20-
built at {removeSecondsFromISODate(build_info.BUILD_TIMESTAMP)} by{" "}
22+
{t("builtAtBy", { builtAt: removeSecondsFromISODate(build_info.BUILD_TIMESTAMP) })}{" "}
2123
<ExternalLink absoluteHref={"https://github.com/" + build_info.GITHUB_ACTOR}>
2224
<Typography fontSize={14} style={styles.link}>
2325
{build_info.GITHUB_ACTOR}

OwnTube.tv/components/ComboBoxInput.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FlatList, Pressable, StyleSheet, TextInput, View } from "react-native";
22
import { Typography } from "./Typography";
3-
import { useCallback, useMemo, useState } from "react";
3+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import { useTheme } from "@react-navigation/native";
55

66
interface DropDownItem {
@@ -13,6 +13,8 @@ interface ComboBoxInputProps {
1313
onChange: (value: string) => void;
1414
data?: Array<DropDownItem>;
1515
testID: string;
16+
searchable?: boolean;
17+
placeholder?: string;
1618
}
1719

1820
const LIST_ITEM_HEIGHT = 50;
@@ -45,10 +47,18 @@ const DropdownItem = ({
4547
);
4648
};
4749

48-
export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: ComboBoxInputProps) => {
50+
export const ComboBoxInput = ({
51+
value = "",
52+
onChange,
53+
data = [],
54+
testID,
55+
searchable,
56+
placeholder,
57+
}: ComboBoxInputProps) => {
4958
const { colors } = useTheme();
5059
const [inputValue, setInputValue] = useState("");
5160
const [isDropDownVisible, setIsDropDownVisible] = useState(false);
61+
const listRef = useRef<FlatList | null>(null);
5262

5363
const onSelect = (item: { label: string; value: string }) => () => {
5464
onChange(item.value);
@@ -57,22 +67,20 @@ export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: Combo
5767
};
5868

5969
const filteredList = useMemo(() => {
60-
if (!inputValue) {
70+
if (!inputValue || !searchable) {
6171
return data;
6272
}
6373

6474
return data.filter(({ label }) => label.toLowerCase().includes(inputValue.toLowerCase()));
65-
}, [data, inputValue]);
75+
}, [data, inputValue, searchable]);
6676

67-
const initialScrollIndex = useMemo(() => {
77+
useEffect(() => {
6878
if (value) {
69-
const scrollTo = filteredList?.findIndex(({ value: itemValue }) => itemValue === value) || 0;
79+
const idx = filteredList?.findIndex(({ value: itemValue }) => itemValue === value) || 0;
7080

71-
return scrollTo > 0 ? scrollTo : 0;
81+
listRef.current?.scrollToIndex({ index: idx || 0 });
7282
}
73-
74-
return 0;
75-
}, [filteredList, value]);
83+
}, [value]);
7684

7785
const renderItem = useCallback(
7886
({ item }: { item: DropDownItem }) => <DropdownItem item={item} onSelect={onSelect} value={value} />,
@@ -82,7 +90,8 @@ export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: Combo
8290
return (
8391
<View testID={testID} accessible={false} style={styles.container}>
8492
<TextInput
85-
placeholder="Search instances..."
93+
editable={searchable}
94+
placeholder={placeholder}
8695
placeholderTextColor={colors.text}
8796
style={[{ color: colors.primary, backgroundColor: colors.card, borderColor: colors.primary }, styles.input]}
8897
onFocus={() => setIsDropDownVisible(true)}
@@ -105,10 +114,10 @@ export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: Combo
105114
]}
106115
>
107116
<FlatList
117+
ref={listRef}
108118
data={filteredList}
109119
renderItem={renderItem}
110120
extraData={filteredList?.length}
111-
initialScrollIndex={initialScrollIndex}
112121
keyExtractor={({ value }) => value}
113122
getItemLayout={(_, index) => ({
114123
length: LIST_ITEM_HEIGHT,

0 commit comments

Comments
 (0)