Skip to content

Commit

Permalink
Merge pull request #105 from mykhailodanilenko/feature/add-i18n
Browse files Browse the repository at this point in the history
Add i18n
  • Loading branch information
mykhailodanilenko authored Jul 18, 2024
2 parents 9b3a50b + 88a7792 commit bcb68cc
Show file tree
Hide file tree
Showing 36 changed files with 610 additions and 99 deletions.
3 changes: 2 additions & 1 deletion OwnTube.tv/api/instanceSearchApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from "axios";
import { PeertubeInstance } from "./models";
import i18n from "../i18n";

export class InstanceSearchApi {
private instance!: AxiosInstance;
Expand Down Expand Up @@ -37,7 +38,7 @@ export class InstanceSearchApi {
});
return response.data;
} catch (error: unknown) {
throw new Error(`Failed to fetch instances: ${(error as Error).message}`);
throw new Error(i18n.t("errors.failedToFetchInstances", { error: (error as Error).message }));
}
}
}
Expand Down
15 changes: 7 additions & 8 deletions OwnTube.tv/api/peertubeVideosApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios, { AxiosInstance } from "axios";
import { VideosCommonQuery } from "@peertube/peertube-types";
import { Video } from "@peertube/peertube-types/peertube-models/videos/video.model";
import { GetVideosVideo } from "./models";
import i18n from "../i18n";

/**
* Get videos from the PeerTube backend `/api/v1/videos` API
Expand All @@ -20,7 +21,7 @@ export class PeertubeVideosApi {
private _maxChunkSize!: number;
set maxChunkSize(value: number) {
if (!(value > 0 && value <= 100)) {
throw new Error("The maximum number of videos to fetch in a single request is 100");
throw new Error("errors.maxVideosPerChunk");
}
this._maxChunkSize = value;
}
Expand Down Expand Up @@ -70,7 +71,7 @@ export class PeertubeVideosApi {
});
return response.data.total as number;
} catch (error: unknown) {
throw new Error(`Failed to fetch total number of videos from PeerTube API: ${(error as Error).message}`);
throw new Error(i18n.t("errors.failedToFetchTotalVids", { error: (error as Error).message }));
}
}

Expand All @@ -92,7 +93,7 @@ export class PeertubeVideosApi {
})
).data.data as Required<GetVideosVideo>[];
} catch (error: unknown) {
throw new Error(`Failed to fetch videos from PeerTube API: ${(error as Error).message}`);
throw new Error(i18n.t("errors.failedToFetchVids", { error: (error as Error).message }));
}
} else {
let rawTotal = -1;
Expand All @@ -104,17 +105,15 @@ export class PeertubeVideosApi {
fetchCount = rawTotal - offset;
if (this.debugLogging) {
console.debug(
`We would exceed the total available ${rawTotal} videos with chunk size ${this.maxChunkSize}, so fetching only ${fetchCount} videos to reach the total`,
i18n.t("errors.maxTotalToBeExceeded", { rawTotal, maxChunkSize: this.maxChunkSize, fetchCount }),
);
}
}
const maxLimitToBeExceeded = rawVideos.length + fetchCount > limit;
if (maxLimitToBeExceeded) {
fetchCount = limit - offset;
if (this.debugLogging) {
console.debug(
`We would exceed max limit of ${limit} videos, so fetching only ${fetchCount} additional videos to reach the limit`,
);
console.debug(i18n.t("errors.maxLimitToBeExceeded", { limit, fetchCount }));
}
}
try {
Expand Down Expand Up @@ -162,7 +161,7 @@ export class PeertubeVideosApi {

return response.data;
} catch (error: unknown) {
throw new Error(`Failed to fetch videos from PeerTube API: ${(error as Error).message}`);
throw new Error(i18n.t("errors.failedToFetchVideo", { error: (error as Error).message, id }));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion OwnTube.tv/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
"bundler": "metro",
"favicon": "assets/faviconLight-48x48.png"
},
"plugins": ["expo-router", "expo-font"]
"plugins": ["expo-router", "expo-font", "expo-localization"]
}
}
8 changes: 6 additions & 2 deletions OwnTube.tv/app/(home)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useTheme } from "@react-navigation/native";
import { StyleSheet, View } from "react-native";
import { useRecentInstances } from "../../hooks";
import { RootStackParams } from "../_layout";
import { useTranslation } from "react-i18next";

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

const getSourceAndRedirect = async () => {
if (backend) {
Expand All @@ -39,7 +41,7 @@ export default function index() {
useCallback(() => {
if (backend) {
navigation.setOptions({
title: `OwnTube.tv@${backend}`,
title: `${t("appName")}@${backend}`,
headerRight: () => (
<View style={styles.headerControls}>
<Link
Expand Down Expand Up @@ -69,7 +71,9 @@ export default function index() {
return (
<>
<Head>
<title>OwnTube.tv@{backend || ""}</title>
<title>
{t("appName")}@{backend || ""}
</title>
<meta name="description" content="OwnTube.tv homepage" />
</Head>
{!!backend && <HomeScreen />}
Expand Down
7 changes: 5 additions & 2 deletions OwnTube.tv/app/(home)/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { SettingsScreen } from "../../screens";
import Head from "expo-router/head";
import { useTranslation } from "react-i18next";

export default function settings() {
const { t } = useTranslation();

return (
<>
<Head>
<title>Settings</title>
<meta name="description" content="OwnTube.tv settings" />
<title>{t("settingsPageTitle")}</title>
<meta name="description" content={`${t("appName")} ${t("settingsPageTitle").toLowerCase()}`} />
</Head>
<SettingsScreen />
</>
Expand Down
4 changes: 3 additions & 1 deletion OwnTube.tv/app/(home)/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import Head from "expo-router/head";
import { useGetVideoQuery } from "../../api";
import { useLocalSearchParams } from "expo-router";
import { RootStackParams } from "../_layout";
import { useTranslation } from "react-i18next";

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

return (
<>
<Head>
<title>{title}</title>
<meta name="description" content="View video" />
<meta name="description" content={t("viewVideo")} />
</Head>
<VideoScreen />
</>
Expand Down
17 changes: 13 additions & 4 deletions OwnTube.tv/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link, Stack, useLocalSearchParams } from "expo-router";
import { Platform, StyleSheet } from "react-native";
import { ROUTES } from "../types";
import { ROUTES, STORAGE } from "../types";
import { Ionicons } from "@expo/vector-icons";
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { AppConfigContextProvider, ColorSchemeContextProvider, useColorSchemeContext } from "../contexts";
Expand All @@ -9,11 +9,20 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useFonts } from "expo-font";
import Toast from "react-native-toast-message";
import { BuildInfoToast, ClickableHeaderText } from "../components";
import "../i18n";
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import { readFromAsyncStorage } from "../utils";

const RootStack = () => {
const { backend } = useLocalSearchParams();
const { scheme } = useColorSchemeContext();
const theme = scheme === "dark" ? DarkTheme : DefaultTheme;
const { t, i18n } = useTranslation();

useEffect(() => {
readFromAsyncStorage(STORAGE.LOCALE).then(i18n.changeLanguage);
}, []);

return (
<ThemeProvider value={theme}>
Expand All @@ -26,14 +35,14 @@ const RootStack = () => {
name={"(home)/index"}
options={{
headerBackVisible: false,
title: "OwnTube.tv",
title: t("appName"),
headerLeft: () => <></>,
headerRight: () => <></>,
}}
/>
<Stack.Screen
options={{
title: "Settings",
title: t("settingsPageTitle"),
headerBackVisible: false,
headerLeft: () => (
<Link style={styles.headerButtonLeft} href={{ pathname: "/", params: { backend } }}>
Expand All @@ -43,7 +52,7 @@ const RootStack = () => {
}}
name={`(home)/${ROUTES.SETTINGS}`}
/>
<Stack.Screen options={{ title: "Video", headerShown: false }} name={`(home)/video`} />
<Stack.Screen options={{ title: t("videoPageTitle"), headerShown: false }} name={`(home)/video`} />
</Stack>
<Toast config={{ buildInfo: () => <BuildInfoToast /> }} />
</ThemeProvider>
Expand Down
8 changes: 6 additions & 2 deletions OwnTube.tv/components/AppConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ import { DeviceCapabilities } from "./DeviceCapabilities";
import { StyleSheet, Switch, View } from "react-native";
import { Typography } from "./Typography";
import { useAppConfigContext, useColorSchemeContext } from "../contexts";
import { useTranslation } from "react-i18next";
import { SelectLanguage } from "./SelectLanguage";

export const AppConfig = () => {
const { isDebugMode, setIsDebugMode } = useAppConfigContext();
const { scheme, toggleScheme } = useColorSchemeContext();
const { t } = useTranslation();

return (
<View style={styles.deviceInfoAndToggles}>
<DeviceCapabilities />
<View style={styles.togglesContainer}>
<View style={styles.option}>
<Typography>Debug logging</Typography>
<Typography>{t("debugLogging")}</Typography>
<Switch value={isDebugMode} onValueChange={setIsDebugMode} />
</View>
<View style={styles.option}>
<Typography>Toggle Theme</Typography>
<Typography>{t("toggleTheme")}</Typography>
<Switch value={scheme === "light"} onValueChange={toggleScheme} />
</View>
<SelectLanguage />
</View>
</View>
);
Expand Down
6 changes: 4 additions & 2 deletions OwnTube.tv/components/BuildInfoToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import build_info from "../build-info.json";
import { useTheme } from "@react-navigation/native";
import { removeSecondsFromISODate } from "../utils";
import { ExternalLink } from "./ExternalLink";
import { useTranslation } from "react-i18next";

export const BuildInfoToast = () => {
const { colors } = useTheme();
const { t } = useTranslation();

return (
<View style={[{ backgroundColor: colors.card }, styles.container]}>
<Typography fontSize={14} style={{ userSelect: "none" }}>
Revision{" "}
{t("revision")}{" "}
<ExternalLink absoluteHref={build_info.COMMIT_URL}>
<Typography fontSize={14} style={styles.link}>
{build_info.GITHUB_SHA_SHORT}
</Typography>{" "}
</ExternalLink>
built at {removeSecondsFromISODate(build_info.BUILD_TIMESTAMP)} by{" "}
{t("builtAtBy", { builtAt: removeSecondsFromISODate(build_info.BUILD_TIMESTAMP) })}{" "}
<ExternalLink absoluteHref={"https://github.com/" + build_info.GITHUB_ACTOR}>
<Typography fontSize={14} style={styles.link}>
{build_info.GITHUB_ACTOR}
Expand Down
33 changes: 21 additions & 12 deletions OwnTube.tv/components/ComboBoxInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FlatList, Pressable, StyleSheet, TextInput, View } from "react-native";
import { Typography } from "./Typography";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTheme } from "@react-navigation/native";

interface DropDownItem {
Expand All @@ -13,6 +13,8 @@ interface ComboBoxInputProps {
onChange: (value: string) => void;
data?: Array<DropDownItem>;
testID: string;
searchable?: boolean;
placeholder?: string;
}

const LIST_ITEM_HEIGHT = 50;
Expand Down Expand Up @@ -45,10 +47,18 @@ const DropdownItem = ({
);
};

export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: ComboBoxInputProps) => {
export const ComboBoxInput = ({
value = "",
onChange,
data = [],
testID,
searchable,
placeholder,
}: ComboBoxInputProps) => {
const { colors } = useTheme();
const [inputValue, setInputValue] = useState("");
const [isDropDownVisible, setIsDropDownVisible] = useState(false);
const listRef = useRef<FlatList | null>(null);

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

const filteredList = useMemo(() => {
if (!inputValue) {
if (!inputValue || !searchable) {
return data;
}

return data.filter(({ label }) => label.toLowerCase().includes(inputValue.toLowerCase()));
}, [data, inputValue]);
}, [data, inputValue, searchable]);

const initialScrollIndex = useMemo(() => {
useEffect(() => {
if (value) {
const scrollTo = filteredList?.findIndex(({ value: itemValue }) => itemValue === value) || 0;
const idx = filteredList?.findIndex(({ value: itemValue }) => itemValue === value) || 0;

return scrollTo > 0 ? scrollTo : 0;
listRef.current?.scrollToIndex({ index: idx || 0 });
}

return 0;
}, [filteredList, value]);
}, [value]);

const renderItem = useCallback(
({ item }: { item: DropDownItem }) => <DropdownItem item={item} onSelect={onSelect} value={value} />,
Expand All @@ -82,7 +90,8 @@ export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: Combo
return (
<View testID={testID} accessible={false} style={styles.container}>
<TextInput
placeholder="Search instances..."
editable={searchable}
placeholder={placeholder}
placeholderTextColor={colors.text}
style={[{ color: colors.primary, backgroundColor: colors.card, borderColor: colors.primary }, styles.input]}
onFocus={() => setIsDropDownVisible(true)}
Expand All @@ -105,10 +114,10 @@ export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: Combo
]}
>
<FlatList
ref={listRef}
data={filteredList}
renderItem={renderItem}
extraData={filteredList?.length}
initialScrollIndex={initialScrollIndex}
keyExtractor={({ value }) => value}
getItemLayout={(_, index) => ({
length: LIST_ITEM_HEIGHT,
Expand Down
Loading

0 comments on commit bcb68cc

Please sign in to comment.