Persit selected language & i18n fixes (#510)

This commit is contained in:
Zoe Roux 2024-05-23 15:29:10 +02:00 committed by GitHub
commit cad81b70b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 95 additions and 26 deletions

View File

@ -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();

View File

@ -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;
}; };

View File

@ -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"]
} }

View File

@ -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);
}; };

View File

@ -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

View File

@ -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}
/> />

View File

@ -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>
))} ))}

View File

@ -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

View File

@ -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" }}

View File

@ -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>
)} )}

View File

@ -26,4 +26,8 @@ declare module "i18next" {
returnNull: false; returnNull: false;
resources: { translation: typeof en }; resources: { translation: typeof en };
} }
interface i18n {
systemLanguage: string;
}
} }

View File

@ -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>
); );
}; };

View File

@ -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

View File

@ -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",