mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-08 02:34:16 -04: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