mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05:00 
			
		
		
		
	Persit selected language & i18n fixes (#510)
This commit is contained in:
		
						commit
						cad81b70b6
					
				@ -68,14 +68,18 @@ const clientStorage = {
 | 
			
		||||
 | 
			
		||||
export const clientPersister = createSyncStoragePersister({ storage: clientStorage });
 | 
			
		||||
 | 
			
		||||
const sysLang = getLocales()[0].languageCode ?? "en";
 | 
			
		||||
i18next.use(initReactI18next).init({
 | 
			
		||||
	interpolation: {
 | 
			
		||||
		escapeValue: false,
 | 
			
		||||
	},
 | 
			
		||||
	returnEmptyString: false,
 | 
			
		||||
	fallbackLng: "en",
 | 
			
		||||
	lng: getLocales()[0].languageCode ?? "en",
 | 
			
		||||
	lng: storage.getString("language") ?? sysLang,
 | 
			
		||||
	resources,
 | 
			
		||||
});
 | 
			
		||||
// @ts-expect-error Manually added value
 | 
			
		||||
i18next.systemLanguage = sysLang;
 | 
			
		||||
 | 
			
		||||
const NavigationThemeProvider = ({ children }: { children: ReactNode }) => {
 | 
			
		||||
	const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
@ -18,9 +18,10 @@
 | 
			
		||||
 * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { readCookie } from "@kyoo/models/src/account-internal";
 | 
			
		||||
import i18next, { type InitOptions } from "i18next";
 | 
			
		||||
import type { AppContext, AppInitialProps, AppProps } from "next/app";
 | 
			
		||||
import { type ComponentType, useMemo } from "react";
 | 
			
		||||
import { type ComponentType, useState } from "react";
 | 
			
		||||
import { I18nextProvider } from "react-i18next";
 | 
			
		||||
import resources from "../../../translations";
 | 
			
		||||
 | 
			
		||||
@ -34,19 +35,21 @@ export const withTranslations = (
 | 
			
		||||
		interpolation: {
 | 
			
		||||
			escapeValue: false,
 | 
			
		||||
		},
 | 
			
		||||
		returnEmptyString: false,
 | 
			
		||||
		fallbackLng: "en",
 | 
			
		||||
		resources,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const AppWithTranslations = (props: AppProps) => {
 | 
			
		||||
		const li18n = useMemo(() => {
 | 
			
		||||
		const [li18n] = useState(() => {
 | 
			
		||||
			if (typeof window === "undefined") return i18n;
 | 
			
		||||
			i18next.init({
 | 
			
		||||
				...commonOptions,
 | 
			
		||||
				lng: props.pageProps.__lang,
 | 
			
		||||
				fallbackLng: "en",
 | 
			
		||||
			});
 | 
			
		||||
			i18next.systemLanguage = props.pageProps.__sysLang;
 | 
			
		||||
			return i18next;
 | 
			
		||||
		}, [props.pageProps.__lang]);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return (
 | 
			
		||||
			<I18nextProvider i18n={li18n}>
 | 
			
		||||
@ -56,13 +59,15 @@ export const withTranslations = (
 | 
			
		||||
	};
 | 
			
		||||
	AppWithTranslations.getInitialProps = async (ctx: AppContext) => {
 | 
			
		||||
		const props: AppInitialProps = await AppToTranslate.getInitialProps(ctx);
 | 
			
		||||
		const lng = ctx.router.locale || ctx.router.defaultLocale || "en";
 | 
			
		||||
		const sysLng = ctx.router.locale || ctx.router.defaultLocale || "en";
 | 
			
		||||
		const lng = readCookie(ctx.ctx.req?.headers.cookie, "language") || sysLng;
 | 
			
		||||
		await i18n.init({
 | 
			
		||||
			...commonOptions,
 | 
			
		||||
			lng,
 | 
			
		||||
			fallbackLng: "en",
 | 
			
		||||
		});
 | 
			
		||||
		i18n.systemLanguage = sysLng;
 | 
			
		||||
		props.pageProps.__lang = lng;
 | 
			
		||||
		props.pageProps.__sysLang = sysLng;
 | 
			
		||||
		return props;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,6 @@
 | 
			
		||||
			"~/*": ["src/*"]
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
 | 
			
		||||
	"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../../packages/ui/src/i18n-d.d.ts"],
 | 
			
		||||
	"exclude": ["node_modules"]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,16 @@ export const useUserTheme = (ssrTheme?: "light" | "dark" | "auto") => {
 | 
			
		||||
	return value as "light" | "dark" | "auto";
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setUserTheme = (theme: "light" | "dark" | "auto") => {
 | 
			
		||||
	storage.set("theme", theme);
 | 
			
		||||
	if (Platform.OS === "web") setCookie("theme", theme);
 | 
			
		||||
export const storeData = (key: string, value: string | number | boolean) => {
 | 
			
		||||
	storage.set(key, value);
 | 
			
		||||
	if (Platform.OS === "web") setCookie(key, value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deleteData = (key: string) => {
 | 
			
		||||
	storage.delete(key);
 | 
			
		||||
	if (Platform.OS === "web") setCookie(key, undefined);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setUserTheme = (theme: "light" | "dark" | "auto") => {
 | 
			
		||||
	storeData("theme", theme);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ const SortTrigger = forwardRef<View, PressableProps & { sortKey: string }>(funct
 | 
			
		||||
			{...tooltip(t("browse.sortby-tt"))}
 | 
			
		||||
		>
 | 
			
		||||
			<Icon icon={Sort} {...css({ paddingX: ts(0.5) })} />
 | 
			
		||||
			<P>{t(`browse.sortkey.${sortKey}`)}</P>
 | 
			
		||||
			<P>{t(`browse.sortkey.${sortKey}` as any)}</P>
 | 
			
		||||
		</PressableFeedback>
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
@ -102,7 +102,7 @@ export const BrowseSettings = ({
 | 
			
		||||
					{availableSorts.map((x) => (
 | 
			
		||||
						<Menu.Item
 | 
			
		||||
							key={x}
 | 
			
		||||
							label={t(`browse.sortkey.${x}`)}
 | 
			
		||||
							label={t(`browse.sortkey.${x}` as any)}
 | 
			
		||||
							selected={sortKey === x}
 | 
			
		||||
							icon={
 | 
			
		||||
								x !== SearchSort.Relevance
 | 
			
		||||
 | 
			
		||||
@ -104,7 +104,7 @@ export const WatchListInfo = ({
 | 
			
		||||
					{Object.values(WatchStatusV).map((x) => (
 | 
			
		||||
						<Menu.Item
 | 
			
		||||
							key={x}
 | 
			
		||||
							label={t(`show.watchlistMark.${x.toLowerCase()}`)}
 | 
			
		||||
							label={t(`show.watchlistMark.${x.toLowerCase() as Lowercase<WatchStatusV>}`)}
 | 
			
		||||
							onSelect={() => mutation.mutate(x)}
 | 
			
		||||
							selected={x === status}
 | 
			
		||||
						/>
 | 
			
		||||
 | 
			
		||||
@ -429,7 +429,7 @@ const Description = ({
 | 
			
		||||
						{isLoading ? (
 | 
			
		||||
							<Skeleton {...css({ width: rem(5) })} />
 | 
			
		||||
						) : (
 | 
			
		||||
							<A href={`/genres/${genre.toLowerCase()}`}>{genre}</A>
 | 
			
		||||
							<A href={`/genres/${genre.toLowerCase()}`}>{t(`genres.${genre}`)}</A>
 | 
			
		||||
						)}
 | 
			
		||||
					</Fragment>
 | 
			
		||||
				))}
 | 
			
		||||
@ -481,7 +481,7 @@ const Description = ({
 | 
			
		||||
								{isLoading ? (
 | 
			
		||||
									<Skeleton {...css({ marginBottom: 0 })} />
 | 
			
		||||
								) : (
 | 
			
		||||
									<A href={`/genres/${genre.toLowerCase()}`}>{genre}</A>
 | 
			
		||||
									<A href={`/genres/${genre.toLowerCase()}`}>{t(`genres.${genre}`)}</A>
 | 
			
		||||
								)}
 | 
			
		||||
							</LI>
 | 
			
		||||
						))}
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export const ConnectionError = () => {
 | 
			
		||||
	return (
 | 
			
		||||
		<View {...css({ padding: ts(2) })}>
 | 
			
		||||
			<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
 | 
			
		||||
			<P>{error?.errors[0] ?? t("error.unknown")}</P>
 | 
			
		||||
			<P>{error?.errors[0] ?? t("errors.unknown")}</P>
 | 
			
		||||
			<P>{t("errors.connection-tips")}</P>
 | 
			
		||||
			<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
 | 
			
		||||
			<Button
 | 
			
		||||
 | 
			
		||||
@ -69,7 +69,9 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<>
 | 
			
		||||
			{(displayEmpty.current || query.items?.length !== 0) && <Header title={genre} />}
 | 
			
		||||
			{(displayEmpty.current || query.items?.length !== 0) && (
 | 
			
		||||
				<Header title={t(`genres.${genre}`)} />
 | 
			
		||||
			)}
 | 
			
		||||
			<InfiniteFetchList
 | 
			
		||||
				query={query}
 | 
			
		||||
				layout={{ ...ItemGrid.layout, layout: "horizontal" }}
 | 
			
		||||
 | 
			
		||||
@ -193,7 +193,7 @@ export const ItemDetails = ({
 | 
			
		||||
				{genres && (
 | 
			
		||||
					<ScrollView horizontal contentContainerStyle={{ alignItems: "center" }}>
 | 
			
		||||
						{genres.map((x, i) => (
 | 
			
		||||
							<Chip key={x ?? i} label={x} size="small" {...css({ mX: ts(0.5) })} />
 | 
			
		||||
							<Chip key={x ?? i} label={t(`genres.${x}`)} size="small" {...css({ mX: ts(0.5) })} />
 | 
			
		||||
						))}
 | 
			
		||||
					</ScrollView>
 | 
			
		||||
				)}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								front/packages/ui/src/i18n-d.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								front/packages/ui/src/i18n-d.d.ts
									
									
									
									
										vendored
									
									
								
							@ -26,4 +26,8 @@ declare module "i18next" {
 | 
			
		||||
		returnNull: false;
 | 
			
		||||
		resources: { translation: typeof en };
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface i18n {
 | 
			
		||||
		systemLanguage: string;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ import {
 | 
			
		||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
 | 
			
		||||
import { Children, type ReactElement, type ReactNode } from "react";
 | 
			
		||||
import { type Falsy, View } from "react-native";
 | 
			
		||||
import { px, rem, useYoshiki } from "yoshiki/native";
 | 
			
		||||
import { percent, px, rem, useYoshiki } from "yoshiki/native";
 | 
			
		||||
 | 
			
		||||
export const Preference = ({
 | 
			
		||||
	customIcon,
 | 
			
		||||
@ -79,7 +79,18 @@ export const Preference = ({
 | 
			
		||||
					<SubP>{description}</SubP>
 | 
			
		||||
				</View>
 | 
			
		||||
			</View>
 | 
			
		||||
			<View {...css({ marginX: ts(2), flexDirection: "row", gap: ts(1) })}>{children}</View>
 | 
			
		||||
			<View
 | 
			
		||||
				{...css({
 | 
			
		||||
					marginX: ts(2),
 | 
			
		||||
					flexDirection: "row",
 | 
			
		||||
					justifyContent: "flex-end",
 | 
			
		||||
					gap: ts(1),
 | 
			
		||||
					maxWidth: percent(50),
 | 
			
		||||
					flexWrap: "wrap",
 | 
			
		||||
				})}
 | 
			
		||||
			>
 | 
			
		||||
				{children}
 | 
			
		||||
			</View>
 | 
			
		||||
		</View>
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
 * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { setUserTheme, useUserTheme } from "@kyoo/models";
 | 
			
		||||
import { deleteData, setUserTheme, storeData, useUserTheme } from "@kyoo/models";
 | 
			
		||||
import { Link, Select } from "@kyoo/primitives";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { Preference, SettingsContainer } from "./base";
 | 
			
		||||
@ -35,6 +35,16 @@ export const GeneralSettings = () => {
 | 
			
		||||
	const theme = useUserTheme("auto");
 | 
			
		||||
	const getLanguageName = useLanguageName();
 | 
			
		||||
 | 
			
		||||
	const changeLanguage = (lang: string) => {
 | 
			
		||||
		if (lang === "system") {
 | 
			
		||||
			i18n.changeLanguage(i18n.systemLanguage);
 | 
			
		||||
			deleteData("language");
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		storeData("language", lang);
 | 
			
		||||
		i18n.changeLanguage(lang);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return (
 | 
			
		||||
		<SettingsContainer title={t("settings.general.label")}>
 | 
			
		||||
			<Preference
 | 
			
		||||
@ -57,10 +67,8 @@ export const GeneralSettings = () => {
 | 
			
		||||
			>
 | 
			
		||||
				<Select
 | 
			
		||||
					label={t("settings.general.language.label")}
 | 
			
		||||
					value={i18n.resolvedLanguage!}
 | 
			
		||||
					onValueChange={(value) =>
 | 
			
		||||
						i18n.changeLanguage(value !== "system" ? value : (i18n.options.lng as string))
 | 
			
		||||
					}
 | 
			
		||||
					value={i18n.resolvedLanguage! === i18n.systemLanguage ? "system" : i18n.resolvedLanguage!}
 | 
			
		||||
					onValueChange={(value) => changeLanguage(value)}
 | 
			
		||||
					values={["system", ...Object.keys(i18n.options.resources!)]}
 | 
			
		||||
					getLabel={(key) =>
 | 
			
		||||
						key === "system" ? t("settings.general.language.system") : getLanguageName(key) ?? key
 | 
			
		||||
 | 
			
		||||
@ -61,6 +61,32 @@
 | 
			
		||||
		"switchToGrid": "Switch to grid view",
 | 
			
		||||
		"switchToList": "Switch to list view"
 | 
			
		||||
	},
 | 
			
		||||
	"genres": {
 | 
			
		||||
		"Action": "Action",
 | 
			
		||||
		"Adventure": "Adventure",
 | 
			
		||||
		"Animation": "Animation",
 | 
			
		||||
		"Comedy": "Comedy",
 | 
			
		||||
		"Crime": "Crime",
 | 
			
		||||
		"Documentary": "Documentary",
 | 
			
		||||
		"Drama": "Drama",
 | 
			
		||||
		"Family": "Family",
 | 
			
		||||
		"Fantasy": "Fantasy",
 | 
			
		||||
		"History": "History",
 | 
			
		||||
		"Horror": "Horror",
 | 
			
		||||
		"Music": "Music",
 | 
			
		||||
		"Mystery": "Mystery",
 | 
			
		||||
		"Romance": "Romance",
 | 
			
		||||
		"ScienceFiction": "Science Fiction",
 | 
			
		||||
		"Thriller": "Thriller",
 | 
			
		||||
		"War": "War",
 | 
			
		||||
		"Western": "Western",
 | 
			
		||||
		"Kids": "Kids",
 | 
			
		||||
		"News": "News",
 | 
			
		||||
		"Reality": "Reality",
 | 
			
		||||
		"Soap": "Soap",
 | 
			
		||||
		"Talk": "Talk",
 | 
			
		||||
		"Politics": "Politics"
 | 
			
		||||
	},
 | 
			
		||||
	"misc": {
 | 
			
		||||
		"settings": "Settings",
 | 
			
		||||
		"prev-page": "Previous page",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user