mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-11-21 05:53:11 -05:00
Add automatic language detection and language setting
This commit is contained in:
parent
079cc6b4f9
commit
39cfd501ac
@ -1,4 +1,5 @@
|
||||
import type { ExpoConfig } from "expo/config";
|
||||
import { supportedLanguages } from "~/providers/translations.compile";
|
||||
|
||||
const IS_DEV = process.env.APP_VARIANT === "development";
|
||||
|
||||
@ -75,6 +76,12 @@ export const expo: ExpoConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"react-native-localization-settings",
|
||||
{
|
||||
languages: supportedLanguages,
|
||||
}
|
||||
]
|
||||
],
|
||||
experiments: {
|
||||
typedRoutes: true,
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
"expo-splash-screen": "^31.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-updates": "~29.0.11",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jassub": "^1.8.6",
|
||||
"langmap": "^0.0.16",
|
||||
@ -36,6 +37,7 @@
|
||||
"react-i18next": "^16.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-get-random-values": "^2.0.0",
|
||||
"react-native-localization-settings": "^1.2.0",
|
||||
"react-native-mmkv": "^3.3.3",
|
||||
"react-native-nitro-modules": "^0.30.2",
|
||||
"react-native-reanimated": "~4.1.2",
|
||||
@ -927,6 +929,8 @@
|
||||
|
||||
"i18next": ["i18next@25.6.0", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||
|
||||
"i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@ -1259,6 +1263,8 @@
|
||||
|
||||
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
|
||||
|
||||
"react-native-localization-settings": ["react-native-localization-settings@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-pxX/mfokqjwIdb1zINuN6DLL4PeVHTaIGz2Tk833tS94fmpsSuPoYnkCmtXsfvZjxhDOSsRceao/JutJbIlpIQ=="],
|
||||
|
||||
"react-native-mmkv": ["react-native-mmkv@3.3.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-GMsfOmNzx0p5+CtrCFRVtpOOMYNJXuksBVARSQrCFaZwjUyHJdQzcN900GGaFFNTxw2fs8s5Xje//RDKj9+PZA=="],
|
||||
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.30.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-+/uVS7FQwOiKYZQERMIvBRv5/X3CVHrFG6Nr/kIhVfVxGeUimHnBz7cgA97lJKIn7AKDRWL+UjLedW8pGOt0dg=="],
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
"expo-splash-screen": "^31.0.10",
|
||||
"expo-status-bar": "~3.0.8",
|
||||
"expo-updates": "~29.0.11",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jassub": "^1.8.6",
|
||||
"langmap": "^0.0.16",
|
||||
@ -47,6 +48,7 @@
|
||||
"react-i18next": "^16.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-get-random-values": "^2.0.0",
|
||||
"react-native-localization-settings": "^1.2.0",
|
||||
"react-native-mmkv": "^3.3.3",
|
||||
"react-native-nitro-modules": "^0.30.2",
|
||||
"react-native-reanimated": "~4.1.2",
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { deleteData, setUserTheme, storeData, useUserTheme } from "@kyoo/models";
|
||||
import { Link, Select } from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg";
|
||||
import Language from "@material-symbols/svg-400/outlined/language.svg";
|
||||
|
||||
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||
import { useLanguageName } from "../utils";
|
||||
|
||||
export const GeneralSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
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
|
||||
icon={Theme}
|
||||
label={t("settings.general.theme.label")}
|
||||
description={t("settings.general.theme.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.theme.label")}
|
||||
value={theme}
|
||||
onValueChange={(value) => setUserTheme(value)}
|
||||
values={["auto", "light", "dark"]}
|
||||
getLabel={(key) => t(`settings.general.theme.${key}`)}
|
||||
/>
|
||||
</Preference>
|
||||
<Preference
|
||||
icon={Language}
|
||||
label={t("settings.general.language.label")}
|
||||
description={t("settings.general.language.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.language.label")}
|
||||
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)
|
||||
}
|
||||
/>
|
||||
</Preference>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const About = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.about.label")}>
|
||||
<Link href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk" target="_blank">
|
||||
<Preference
|
||||
icon={Android}
|
||||
label={t("settings.about.android-app.label")}
|
||||
description={t("settings.about.android-app.description")}
|
||||
/>
|
||||
</Link>
|
||||
<Link href="https://github.com/zoriya/kyoo" target="_blank">
|
||||
<Preference
|
||||
icon={Public}
|
||||
label={t("settings.about.git.label")}
|
||||
description={t("settings.about.git.description")}
|
||||
/>
|
||||
</Link>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
@ -128,8 +128,7 @@
|
||||
},
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"description": "Set the language of your application",
|
||||
"system": "System"
|
||||
"description": "Set the language of your application"
|
||||
}
|
||||
},
|
||||
"playback": {
|
||||
|
||||
3
front/src/providers/translations-detector.tsx
Normal file
3
front/src/providers/translations-detector.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { createLanguageDetector } from "react-native-localization-settings";
|
||||
|
||||
export const languageDetector = createLanguageDetector({});
|
||||
7
front/src/providers/translations-detector.web.tsx
Normal file
7
front/src/providers/translations-detector.web.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
export const languageDetector = new LanguageDetector(null, {
|
||||
order: ["querystring", "cookie", "navigator", "path", "subdomain"],
|
||||
caches: ["cookie"],
|
||||
cookieMinutes: 525600, // 1 years
|
||||
});
|
||||
@ -3,11 +3,12 @@ import { type ReactNode, useMemo } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { setServerData } from "~/utils";
|
||||
import { resources, supportedLanguages } from "./translations.compile";
|
||||
import { languageDetector } from "./translations-detector";
|
||||
|
||||
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const val = useMemo(() => {
|
||||
const i18n = i18next.createInstance();
|
||||
i18n.init({
|
||||
i18n.use(languageDetector).init({
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
@ -4,24 +4,27 @@ import { type ReactNode, useMemo } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { getServerData } from "~/utils";
|
||||
import { supportedLanguages } from "./translations.compile";
|
||||
import { languageDetector } from "./translations-detector";
|
||||
|
||||
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const val = useMemo(() => {
|
||||
const i18n = i18next.createInstance();
|
||||
// TODO: use https://github.com/i18next/i18next-browser-languageDetector
|
||||
i18n.use(HttpApi).init<HttpBackendOptions>({
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
returnEmptyString: false,
|
||||
fallbackLng: "en",
|
||||
load: "currentOnly",
|
||||
supportedLngs: supportedLanguages,
|
||||
// we don't need to cache resources since we always get a fresh one from ssr
|
||||
backend: {
|
||||
loadPath: "/translations/{{lng}}.json",
|
||||
},
|
||||
});
|
||||
i18n
|
||||
.use(HttpApi)
|
||||
.use(languageDetector)
|
||||
.init<HttpBackendOptions>({
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
returnEmptyString: false,
|
||||
fallbackLng: "en",
|
||||
load: "currentOnly",
|
||||
supportedLngs: supportedLanguages,
|
||||
// we don't need to cache resources since we always get a fresh one from ssr
|
||||
backend: {
|
||||
loadPath: "/translations/{{lng}}.json",
|
||||
},
|
||||
});
|
||||
i18n.services.resourceStore.data = getServerData("translations");
|
||||
return i18n;
|
||||
}, []);
|
||||
|
||||
72
front/src/ui/settings/general.tsx
Normal file
72
front/src/ui/settings/general.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import Theme from "@material-symbols/svg-400/outlined/dark_mode.svg";
|
||||
import Language from "@material-symbols/svg-400/outlined/language.svg";
|
||||
import Android from "@material-symbols/svg-400/rounded/android.svg";
|
||||
import Public from "@material-symbols/svg-400/rounded/public.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, Select } from "~/primitives";
|
||||
import { supportedLanguages } from "~/providers/translations.compile";
|
||||
import { useLanguageName } from "~/track-utils";
|
||||
import { Preference, SettingsContainer } from "./base";
|
||||
|
||||
export const GeneralSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
// const theme = useUserTheme("auto");
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.general.label")}>
|
||||
{/* <Preference */}
|
||||
{/* icon={Theme} */}
|
||||
{/* label={t("settings.general.theme.label")} */}
|
||||
{/* description={t("settings.general.theme.description")} */}
|
||||
{/* > */}
|
||||
{/* <Select */}
|
||||
{/* label={t("settings.general.theme.label")} */}
|
||||
{/* value={theme} */}
|
||||
{/* onValueChange={(value) => setUserTheme(value)} */}
|
||||
{/* values={["auto", "light", "dark"]} */}
|
||||
{/* getLabel={(key) => t(`settings.general.theme.${key}`)} */}
|
||||
{/* /> */}
|
||||
{/* </Preference> */}
|
||||
<Preference
|
||||
icon={Language}
|
||||
label={t("settings.general.language.label")}
|
||||
description={t("settings.general.language.description")}
|
||||
>
|
||||
<Select
|
||||
label={t("settings.general.language.label")}
|
||||
value={i18n.resolvedLanguage!}
|
||||
onValueChange={(value) => i18n.changeLanguage(value)}
|
||||
values={supportedLanguages}
|
||||
getLabel={(key) => getLanguageName(key) ?? key}
|
||||
/>
|
||||
</Preference>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const About = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsContainer title={t("settings.about.label")}>
|
||||
<Link
|
||||
href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk"
|
||||
target="_blank"
|
||||
>
|
||||
<Preference
|
||||
icon={Android}
|
||||
label={t("settings.about.android-app.label")}
|
||||
description={t("settings.about.android-app.description")}
|
||||
/>
|
||||
</Link>
|
||||
<Link href="https://github.com/zoriya/kyoo" target="_blank">
|
||||
<Preference
|
||||
icon={Public}
|
||||
label={t("settings.about.git.label")}
|
||||
description={t("settings.about.git.description")}
|
||||
/>
|
||||
</Link>
|
||||
</SettingsContainer>
|
||||
);
|
||||
};
|
||||
@ -2,7 +2,7 @@ import { ScrollView } from "react-native";
|
||||
import { ts } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
// import { AccountSettings } from "./account";
|
||||
// import { About, GeneralSettings } from "./general";
|
||||
import { About, GeneralSettings } from "./general";
|
||||
// import { OidcSettings } from "./oidc";
|
||||
// import { PlaybackSettings } from "./playback";
|
||||
|
||||
@ -10,11 +10,11 @@ export const SettingsPage = () => {
|
||||
const account = useAccount();
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
|
||||
{/* <GeneralSettings /> */}
|
||||
<GeneralSettings />
|
||||
{/* {account && <PlaybackSettings />} */}
|
||||
{/* {account && <AccountSettings />} */}
|
||||
{/* {account && <OidcSettings />} */}
|
||||
{/* <About /> */}
|
||||
<About />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user