Add translation provider with http & fs backend

This commit is contained in:
Zoe Roux 2025-02-14 22:55:34 +01:00
parent 23f42f247d
commit 31c183ecf1
No known key found for this signature in database
34 changed files with 118 additions and 77 deletions

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { type LibraryItem, LibraryItemP } from "~/models";
import { P } from "~/primitives";
@ -9,13 +11,17 @@ export async function loader() {
export default function Header() {
const { css } = useYoshiki();
const { t } = useTranslation();
return (
<View>
<P>{t("home.recommended")}</P>
<Fetch
query={Header.query()}
Render={({ name }) => <P {...css({ bg: "red" })}>{name}</P>}
Loader={() => <P>Loading</P>}
/>
</View>
);
}

View File

@ -1,6 +1,22 @@
import { createMiddleware, setServerData } from "one";
import { supportedLanguages } from "~/providers/translations.ssr";
export default createMiddleware(({ request, next }) => {
const systemLanguage = request.headers
.get("accept-languages")
?.split(",")
.map((x) => {
const [lang, q] = x.trim().split(";q=");
return [lang, q ? Number.parseFloat(q) : 1] as const;
})
.sort(([_, q1], [__, q2]) => q1 - q2)
.flatMap(([lang]) => {
const [base, spec] = lang.split("-");
if (spec) return [lang, base];
return [lang];
})
.find((x) => supportedLanguages.includes(x));
setServerData("systemLanguage", systemLanguage);
setServerData("cookies", request.headers.get("Cookies") ?? "");
return next();
});

View File

@ -0,0 +1,5 @@
import { supportedLanguages } from "~/providers/translations.ssr";
export default (): Response => {
return Response.json(supportedLanguages);
};

View File

@ -11,6 +11,8 @@
"expo": "~52.0.32",
"expo-build-properties": "^0.13.2",
"expo-localization": "^16.0.1",
"i18next-fs-backend": "^2.6.0",
"i18next-http-backend": "^3.0.2",
"jotai": "^2.11.3",
"one": "1.1.437",
"react": "^19.0.0",
@ -980,7 +982,7 @@
"create-vxrn": ["create-vxrn@1.1.437", "", { "dependencies": { "@tamagui/build": "^1.124.1", "@types/validate-npm-package-name": "^4.0.2", "@vxrn/utils": "1.1.437", "ansis": "^3.1.0", "async-retry": "1.3.1", "citty": "^0.1.6", "cpy": "^11.0.1", "fs-extra": "^11.2.0", "prompts": "^2.4.2", "rimraf": "^5.0.1", "validate-npm-package-name": "3.0.0", "yocto-spinner": "^0.1.0" }, "bin": { "create-vxrn": "run.js" } }, "sha512-mWgLLCpLGMt0IANR4j7KTZrUYGN2rv3gn0eHCBY7gknFn5MshvBQvgWyJCOIQsF2DKijc+/FMtmrBNudJduw/A=="],
"cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
"cross-fetch": ["cross-fetch@4.0.0", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@ -1244,6 +1246,10 @@
"i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
"i18next-fs-backend": ["i18next-fs-backend@2.6.0", "", {}, "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw=="],
"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=="],
"ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
@ -2218,6 +2224,8 @@
"expo-modules-autolinking/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"fbjs/cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
"fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],

9
front/i18n-d.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import "i18next";
import type en from "./public/translations/en.json";
declare module "i18next" {
interface CustomTypeOptions {
returnNull: false;
resources: { translation: typeof en };
}
}

View File

@ -20,6 +20,8 @@
"expo": "~52.0.32",
"expo-build-properties": "^0.13.2",
"expo-localization": "^16.0.1",
"i18next-fs-backend": "^2.6.0",
"i18next-http-backend": "^3.0.2",
"jotai": "^2.11.3",
"one": "1.1.437",
"react": "^19.0.0",

View File

@ -1,33 +1,9 @@
/*
* 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 "i18next";
import type en from "../../../translations/en.json";
import type en from "./public/translations/en.json";
declare module "i18next" {
interface CustomTypeOptions {
returnNull: false;
resources: { translation: typeof en };
}
interface i18n {
systemLanguage: string;
}
}

View File

@ -95,7 +95,6 @@ export const Link = ({
} & PressableProps) => {
const linkProps = useLinkTo({ href: href ?? "#", replace });
console.warn(children);
return (
<PressableFeedback
{...linkProps}

View File

@ -1,11 +1,12 @@
import { HydrationBoundary, QueryClientProvider } from "@tanstack/react-query";
import { getServerData } from "one";
import { type ReactNode, useState } from "react";
import { type ReactNode, useMemo, useState } from "react";
// import { useUserTheme } from "@kyoo/models";
import { ThemeSelector } from "~/primitives/theme";
import { createQueryClient } from "~/query";
import { AccountProvider } from "./account-provider";
import { ErrorConsumer, ErrorProvider } from "./error-provider";
import { TranslationsProvider } from "./translations.ssr";
const QueryProvider = ({ children }: { children: ReactNode }) => {
const [queryClient] = useState(() => createQueryClient());
@ -33,7 +34,9 @@ export const Providers = ({ children }: { children: ReactNode }) => {
<ThemeProvider>
<ErrorProvider>
<AccountProvider>
<TranslationsProvider>
<ErrorConsumer scope="root">{children}</ErrorConsumer>
</TranslationsProvider>
</AccountProvider>
</ErrorProvider>
</ThemeProvider>

View File

@ -0,0 +1,34 @@
import { readdirSync } from "node:fs";
import i18next from "i18next";
import FsBackend, { type FsBackendOptions } from "i18next-fs-backend";
import { getServerData, setServerData } from "one";
import { type ReactNode, useMemo } from "react";
import { I18nextProvider } from "react-i18next";
export const supportedLanguages = readdirSync(
new URL("../../public/translations", import.meta.url),
).map((x) => x.replace(".json", ""));
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
const val = useMemo(() => {
const i18n = i18next.createInstance();
i18n.use(FsBackend).init<FsBackendOptions>({
interpolation: {
escapeValue: false,
},
returnEmptyString: false,
fallbackLng: "en",
load: "currentOnly",
lng: getServerData("systemLanguage"),
supportedLngs: supportedLanguages,
initAsync: false,
backend: {
loadPath: `${new URL("../../public/translations", import.meta.url).pathname}/{{lng}}.json`,
},
});
setServerData("supportedLngs", supportedLanguages);
setServerData("translations", i18n.services.resourceStore.data);
return i18n;
}, []);
return <I18nextProvider i18n={val}>{children}</I18nextProvider>;
};

View File

@ -0,0 +1,26 @@
import i18next from "i18next";
import HttpApi, { type HttpBackendOptions } from "i18next-http-backend";
import { getServerData } from "one";
import { type ReactNode, useMemo } from "react";
import { I18nextProvider } from "react-i18next";
export const TranslationsProvider = ({ children }: { children: ReactNode }) => {
const val = useMemo(() => {
const i18n = i18next.createInstance();
i18n.use(HttpApi).init<HttpBackendOptions>({
interpolation: {
escapeValue: false,
},
returnEmptyString: false,
fallbackLng: "en",
load: "currentOnly",
supportedLngs: getServerData("supportedLngs"),
backend: {
loadPath: "/translations/{{lng}}.json",
},
});
i18n.services.resourceStore.data = getServerData("translations");
return i18n;
}, []);
return <I18nextProvider i18n={val}>{children}</I18nextProvider>;
};

View File

@ -5,8 +5,6 @@ import { useYoshiki } from "yoshiki/native";
import { Button, Icon, Link, P, ts } from "~/primitives";
import { useAccount } from "~/providers/account-provider";
console.log(Register);
export const Unauthorized = ({ missing }: { missing: string[] }) => {
const { t } = useTranslation();
const { css } = useYoshiki();

View File

@ -1,41 +0,0 @@
import am from "./am";
import ar from "./ar";
import de from "./de";
import en from "./en";
import es from "./es";
import fr from "./fr";
import it from "./it";
import ko from "./ko";
import ml from "./ml";
import nl from "./nl";
import pl from "./pl";
import pt from "./pt";
import pt_br from "./pt_br";
import ro from "./ro";
import ru from "./ru";
import ta from "./ta";
import tr from "./tr";
import uk from "./uk";
import zh from "./zh";
export default {
am: { translation: am },
ar: { translation: ar },
de: { translation: de },
en: { translation: en },
es: { translation: es },
fr: { translation: fr },
it: { translation: it },
ko: { translation: ko },
ml: { translation: ml },
nl: { translation: nl },
pl: { translation: pl },
pt: { translation: pt },
"pt-BR": { translation: pt_br },
ro: { translation: ro },
ru: { translation: ru },
ta: { translation: ta },
tr: { translation: tr },
uk: { translation: uk },
zh: { translation: zh },
};