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 });
 | 
					export const clientPersister = createSyncStoragePersister({ storage: clientStorage });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sysLang = getLocales()[0].languageCode ?? "en";
 | 
				
			||||||
i18next.use(initReactI18next).init({
 | 
					i18next.use(initReactI18next).init({
 | 
				
			||||||
	interpolation: {
 | 
						interpolation: {
 | 
				
			||||||
		escapeValue: false,
 | 
							escapeValue: false,
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						returnEmptyString: false,
 | 
				
			||||||
	fallbackLng: "en",
 | 
						fallbackLng: "en",
 | 
				
			||||||
	lng: getLocales()[0].languageCode ?? "en",
 | 
						lng: storage.getString("language") ?? sysLang,
 | 
				
			||||||
	resources,
 | 
						resources,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					// @ts-expect-error Manually added value
 | 
				
			||||||
 | 
					i18next.systemLanguage = sysLang;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NavigationThemeProvider = ({ children }: { children: ReactNode }) => {
 | 
					const NavigationThemeProvider = ({ children }: { children: ReactNode }) => {
 | 
				
			||||||
	const theme = useTheme();
 | 
						const theme = useTheme();
 | 
				
			||||||
 | 
				
			|||||||
@ -18,9 +18,10 @@
 | 
				
			|||||||
 * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
					 * 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 i18next, { type InitOptions } from "i18next";
 | 
				
			||||||
import type { AppContext, AppInitialProps, AppProps } from "next/app";
 | 
					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 { I18nextProvider } from "react-i18next";
 | 
				
			||||||
import resources from "../../../translations";
 | 
					import resources from "../../../translations";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -34,19 +35,21 @@ export const withTranslations = (
 | 
				
			|||||||
		interpolation: {
 | 
							interpolation: {
 | 
				
			||||||
			escapeValue: false,
 | 
								escapeValue: false,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							returnEmptyString: false,
 | 
				
			||||||
 | 
							fallbackLng: "en",
 | 
				
			||||||
		resources,
 | 
							resources,
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const AppWithTranslations = (props: AppProps) => {
 | 
						const AppWithTranslations = (props: AppProps) => {
 | 
				
			||||||
		const li18n = useMemo(() => {
 | 
							const [li18n] = useState(() => {
 | 
				
			||||||
			if (typeof window === "undefined") return i18n;
 | 
								if (typeof window === "undefined") return i18n;
 | 
				
			||||||
			i18next.init({
 | 
								i18next.init({
 | 
				
			||||||
				...commonOptions,
 | 
									...commonOptions,
 | 
				
			||||||
				lng: props.pageProps.__lang,
 | 
									lng: props.pageProps.__lang,
 | 
				
			||||||
				fallbackLng: "en",
 | 
					 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
								i18next.systemLanguage = props.pageProps.__sysLang;
 | 
				
			||||||
			return i18next;
 | 
								return i18next;
 | 
				
			||||||
		}, [props.pageProps.__lang]);
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return (
 | 
							return (
 | 
				
			||||||
			<I18nextProvider i18n={li18n}>
 | 
								<I18nextProvider i18n={li18n}>
 | 
				
			||||||
@ -56,13 +59,15 @@ export const withTranslations = (
 | 
				
			|||||||
	};
 | 
						};
 | 
				
			||||||
	AppWithTranslations.getInitialProps = async (ctx: AppContext) => {
 | 
						AppWithTranslations.getInitialProps = async (ctx: AppContext) => {
 | 
				
			||||||
		const props: AppInitialProps = await AppToTranslate.getInitialProps(ctx);
 | 
							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({
 | 
							await i18n.init({
 | 
				
			||||||
			...commonOptions,
 | 
								...commonOptions,
 | 
				
			||||||
			lng,
 | 
								lng,
 | 
				
			||||||
			fallbackLng: "en",
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
							i18n.systemLanguage = sysLng;
 | 
				
			||||||
		props.pageProps.__lang = lng;
 | 
							props.pageProps.__lang = lng;
 | 
				
			||||||
 | 
							props.pageProps.__sysLang = sysLng;
 | 
				
			||||||
		return props;
 | 
							return props;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,6 @@
 | 
				
			|||||||
			"~/*": ["src/*"]
 | 
								"~/*": ["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"]
 | 
						"exclude": ["node_modules"]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,16 @@ export const useUserTheme = (ssrTheme?: "light" | "dark" | "auto") => {
 | 
				
			|||||||
	return value as "light" | "dark" | "auto";
 | 
						return value as "light" | "dark" | "auto";
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const setUserTheme = (theme: "light" | "dark" | "auto") => {
 | 
					export const storeData = (key: string, value: string | number | boolean) => {
 | 
				
			||||||
	storage.set("theme", theme);
 | 
						storage.set(key, value);
 | 
				
			||||||
	if (Platform.OS === "web") setCookie("theme", theme);
 | 
						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"))}
 | 
								{...tooltip(t("browse.sortby-tt"))}
 | 
				
			||||||
		>
 | 
							>
 | 
				
			||||||
			<Icon icon={Sort} {...css({ paddingX: ts(0.5) })} />
 | 
								<Icon icon={Sort} {...css({ paddingX: ts(0.5) })} />
 | 
				
			||||||
			<P>{t(`browse.sortkey.${sortKey}`)}</P>
 | 
								<P>{t(`browse.sortkey.${sortKey}` as any)}</P>
 | 
				
			||||||
		</PressableFeedback>
 | 
							</PressableFeedback>
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -102,7 +102,7 @@ export const BrowseSettings = ({
 | 
				
			|||||||
					{availableSorts.map((x) => (
 | 
										{availableSorts.map((x) => (
 | 
				
			||||||
						<Menu.Item
 | 
											<Menu.Item
 | 
				
			||||||
							key={x}
 | 
												key={x}
 | 
				
			||||||
							label={t(`browse.sortkey.${x}`)}
 | 
												label={t(`browse.sortkey.${x}` as any)}
 | 
				
			||||||
							selected={sortKey === x}
 | 
												selected={sortKey === x}
 | 
				
			||||||
							icon={
 | 
												icon={
 | 
				
			||||||
								x !== SearchSort.Relevance
 | 
													x !== SearchSort.Relevance
 | 
				
			||||||
 | 
				
			|||||||
@ -104,7 +104,7 @@ export const WatchListInfo = ({
 | 
				
			|||||||
					{Object.values(WatchStatusV).map((x) => (
 | 
										{Object.values(WatchStatusV).map((x) => (
 | 
				
			||||||
						<Menu.Item
 | 
											<Menu.Item
 | 
				
			||||||
							key={x}
 | 
												key={x}
 | 
				
			||||||
							label={t(`show.watchlistMark.${x.toLowerCase()}`)}
 | 
												label={t(`show.watchlistMark.${x.toLowerCase() as Lowercase<WatchStatusV>}`)}
 | 
				
			||||||
							onSelect={() => mutation.mutate(x)}
 | 
												onSelect={() => mutation.mutate(x)}
 | 
				
			||||||
							selected={x === status}
 | 
												selected={x === status}
 | 
				
			||||||
						/>
 | 
											/>
 | 
				
			||||||
 | 
				
			|||||||
@ -429,7 +429,7 @@ const Description = ({
 | 
				
			|||||||
						{isLoading ? (
 | 
											{isLoading ? (
 | 
				
			||||||
							<Skeleton {...css({ width: rem(5) })} />
 | 
												<Skeleton {...css({ width: rem(5) })} />
 | 
				
			||||||
						) : (
 | 
											) : (
 | 
				
			||||||
							<A href={`/genres/${genre.toLowerCase()}`}>{genre}</A>
 | 
												<A href={`/genres/${genre.toLowerCase()}`}>{t(`genres.${genre}`)}</A>
 | 
				
			||||||
						)}
 | 
											)}
 | 
				
			||||||
					</Fragment>
 | 
										</Fragment>
 | 
				
			||||||
				))}
 | 
									))}
 | 
				
			||||||
@ -481,7 +481,7 @@ const Description = ({
 | 
				
			|||||||
								{isLoading ? (
 | 
													{isLoading ? (
 | 
				
			||||||
									<Skeleton {...css({ marginBottom: 0 })} />
 | 
														<Skeleton {...css({ marginBottom: 0 })} />
 | 
				
			||||||
								) : (
 | 
													) : (
 | 
				
			||||||
									<A href={`/genres/${genre.toLowerCase()}`}>{genre}</A>
 | 
														<A href={`/genres/${genre.toLowerCase()}`}>{t(`genres.${genre}`)}</A>
 | 
				
			||||||
								)}
 | 
													)}
 | 
				
			||||||
							</LI>
 | 
												</LI>
 | 
				
			||||||
						))}
 | 
											))}
 | 
				
			||||||
 | 
				
			|||||||
@ -64,7 +64,7 @@ export const ConnectionError = () => {
 | 
				
			|||||||
	return (
 | 
						return (
 | 
				
			||||||
		<View {...css({ padding: ts(2) })}>
 | 
							<View {...css({ padding: ts(2) })}>
 | 
				
			||||||
			<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
 | 
								<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>
 | 
								<P>{t("errors.connection-tips")}</P>
 | 
				
			||||||
			<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
 | 
								<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
 | 
				
			||||||
			<Button
 | 
								<Button
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,9 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<>
 | 
							<>
 | 
				
			||||||
			{(displayEmpty.current || query.items?.length !== 0) && <Header title={genre} />}
 | 
								{(displayEmpty.current || query.items?.length !== 0) && (
 | 
				
			||||||
 | 
									<Header title={t(`genres.${genre}`)} />
 | 
				
			||||||
 | 
								)}
 | 
				
			||||||
			<InfiniteFetchList
 | 
								<InfiniteFetchList
 | 
				
			||||||
				query={query}
 | 
									query={query}
 | 
				
			||||||
				layout={{ ...ItemGrid.layout, layout: "horizontal" }}
 | 
									layout={{ ...ItemGrid.layout, layout: "horizontal" }}
 | 
				
			||||||
 | 
				
			|||||||
@ -193,7 +193,7 @@ export const ItemDetails = ({
 | 
				
			|||||||
				{genres && (
 | 
									{genres && (
 | 
				
			||||||
					<ScrollView horizontal contentContainerStyle={{ alignItems: "center" }}>
 | 
										<ScrollView horizontal contentContainerStyle={{ alignItems: "center" }}>
 | 
				
			||||||
						{genres.map((x, i) => (
 | 
											{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>
 | 
										</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;
 | 
							returnNull: false;
 | 
				
			||||||
		resources: { translation: typeof en };
 | 
							resources: { translation: typeof en };
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						interface i18n {
 | 
				
			||||||
 | 
							systemLanguage: string;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,7 @@ import {
 | 
				
			|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
 | 
					import { useMutation, useQueryClient } from "@tanstack/react-query";
 | 
				
			||||||
import { Children, type ReactElement, type ReactNode } from "react";
 | 
					import { Children, type ReactElement, type ReactNode } from "react";
 | 
				
			||||||
import { type Falsy, View } from "react-native";
 | 
					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 = ({
 | 
					export const Preference = ({
 | 
				
			||||||
	customIcon,
 | 
						customIcon,
 | 
				
			||||||
@ -79,7 +79,18 @@ export const Preference = ({
 | 
				
			|||||||
					<SubP>{description}</SubP>
 | 
										<SubP>{description}</SubP>
 | 
				
			||||||
				</View>
 | 
									</View>
 | 
				
			||||||
			</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>
 | 
							</View>
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@
 | 
				
			|||||||
 * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
 | 
					 * 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 { Link, Select } from "@kyoo/primitives";
 | 
				
			||||||
import { useTranslation } from "react-i18next";
 | 
					import { useTranslation } from "react-i18next";
 | 
				
			||||||
import { Preference, SettingsContainer } from "./base";
 | 
					import { Preference, SettingsContainer } from "./base";
 | 
				
			||||||
@ -35,6 +35,16 @@ export const GeneralSettings = () => {
 | 
				
			|||||||
	const theme = useUserTheme("auto");
 | 
						const theme = useUserTheme("auto");
 | 
				
			||||||
	const getLanguageName = useLanguageName();
 | 
						const getLanguageName = useLanguageName();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const changeLanguage = (lang: string) => {
 | 
				
			||||||
 | 
							if (lang === "system") {
 | 
				
			||||||
 | 
								i18n.changeLanguage(i18n.systemLanguage);
 | 
				
			||||||
 | 
								deleteData("language");
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							storeData("language", lang);
 | 
				
			||||||
 | 
							i18n.changeLanguage(lang);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
		<SettingsContainer title={t("settings.general.label")}>
 | 
							<SettingsContainer title={t("settings.general.label")}>
 | 
				
			||||||
			<Preference
 | 
								<Preference
 | 
				
			||||||
@ -57,10 +67,8 @@ export const GeneralSettings = () => {
 | 
				
			|||||||
			>
 | 
								>
 | 
				
			||||||
				<Select
 | 
									<Select
 | 
				
			||||||
					label={t("settings.general.language.label")}
 | 
										label={t("settings.general.language.label")}
 | 
				
			||||||
					value={i18n.resolvedLanguage!}
 | 
										value={i18n.resolvedLanguage! === i18n.systemLanguage ? "system" : i18n.resolvedLanguage!}
 | 
				
			||||||
					onValueChange={(value) =>
 | 
										onValueChange={(value) => changeLanguage(value)}
 | 
				
			||||||
						i18n.changeLanguage(value !== "system" ? value : (i18n.options.lng as string))
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					values={["system", ...Object.keys(i18n.options.resources!)]}
 | 
										values={["system", ...Object.keys(i18n.options.resources!)]}
 | 
				
			||||||
					getLabel={(key) =>
 | 
										getLabel={(key) =>
 | 
				
			||||||
						key === "system" ? t("settings.general.language.system") : getLanguageName(key) ?? key
 | 
											key === "system" ? t("settings.general.language.system") : getLanguageName(key) ?? key
 | 
				
			||||||
 | 
				
			|||||||
@ -61,6 +61,32 @@
 | 
				
			|||||||
		"switchToGrid": "Switch to grid view",
 | 
							"switchToGrid": "Switch to grid view",
 | 
				
			||||||
		"switchToList": "Switch to list 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": {
 | 
						"misc": {
 | 
				
			||||||
		"settings": "Settings",
 | 
							"settings": "Settings",
 | 
				
			||||||
		"prev-page": "Previous page",
 | 
							"prev-page": "Previous page",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user