From fe816aa276ad4df19abf2e1b0afa1a8e9d966c57 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 14 Feb 2025 22:55:34 +0100 Subject: [PATCH] Add translation provider with http & fs backend --- front/app/(app)/index.tsx | 16 ++++++--- front/app/_middleware.ts | 16 +++++++++ front/app/translations+api.ts | 5 +++ front/bun.lock | 10 +++++- front/i18n-d.d.ts | 9 +++++ front/package.json | 2 ++ front/packages/ui/src/i18n-d.d.ts | 26 +------------- front/{ => public}/translations/am.json | 0 front/{ => public}/translations/ar.json | 0 front/{ => public}/translations/de.json | 0 front/{ => public}/translations/en.json | 0 front/{ => public}/translations/es.json | 0 front/{ => public}/translations/fr.json | 0 front/{ => public}/translations/is.json | 0 front/{ => public}/translations/it.json | 0 front/{ => public}/translations/ko.json | 0 front/{ => public}/translations/ml.json | 0 front/{ => public}/translations/nl.json | 0 front/{ => public}/translations/pl.json | 0 front/{ => public}/translations/pt.json | 0 front/{ => public}/translations/pt_br.json | 0 front/{ => public}/translations/ro.json | 0 front/{ => public}/translations/ru.json | 0 front/{ => public}/translations/ta.json | 0 front/{ => public}/translations/tr.json | 0 front/{ => public}/translations/uk.json | 0 front/{ => public}/translations/zh.json | 0 front/src/primitives/links.tsx | 1 - front/src/providers/index.tsx | 7 ++-- front/src/providers/translations.ssr.tsx | 34 ++++++++++++++++++ front/src/providers/translations.tsx | 26 ++++++++++++++ front/src/ui/errors/unauthorized.tsx | 2 -- front/translations/index.js | 41 ---------------------- 33 files changed, 118 insertions(+), 77 deletions(-) create mode 100644 front/app/translations+api.ts create mode 100644 front/i18n-d.d.ts rename front/{ => public}/translations/am.json (100%) rename front/{ => public}/translations/ar.json (100%) rename front/{ => public}/translations/de.json (100%) rename front/{ => public}/translations/en.json (100%) rename front/{ => public}/translations/es.json (100%) rename front/{ => public}/translations/fr.json (100%) rename front/{ => public}/translations/is.json (100%) rename front/{ => public}/translations/it.json (100%) rename front/{ => public}/translations/ko.json (100%) rename front/{ => public}/translations/ml.json (100%) rename front/{ => public}/translations/nl.json (100%) rename front/{ => public}/translations/pl.json (100%) rename front/{ => public}/translations/pt.json (100%) rename front/{ => public}/translations/pt_br.json (100%) rename front/{ => public}/translations/ro.json (100%) rename front/{ => public}/translations/ru.json (100%) rename front/{ => public}/translations/ta.json (100%) rename front/{ => public}/translations/tr.json (100%) rename front/{ => public}/translations/uk.json (100%) rename front/{ => public}/translations/zh.json (100%) create mode 100644 front/src/providers/translations.ssr.tsx create mode 100644 front/src/providers/translations.tsx delete mode 100644 front/translations/index.js diff --git a/front/app/(app)/index.tsx b/front/app/(app)/index.tsx index 1fbdf00e..7fea36e4 100644 --- a/front/app/(app)/index.tsx +++ b/front/app/(app)/index.tsx @@ -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 ( -

{name}

} - Loader={() =>

Loading

} - /> + +

{t("home.recommended")}

+

{name}

} + Loader={() =>

Loading

} + /> +
); } diff --git a/front/app/_middleware.ts b/front/app/_middleware.ts index c3e4053b..2bd53614 100644 --- a/front/app/_middleware.ts +++ b/front/app/_middleware.ts @@ -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(); }); diff --git a/front/app/translations+api.ts b/front/app/translations+api.ts new file mode 100644 index 00000000..c0ba2251 --- /dev/null +++ b/front/app/translations+api.ts @@ -0,0 +1,5 @@ +import { supportedLanguages } from "~/providers/translations.ssr"; + +export default (): Response => { + return Response.json(supportedLanguages); +}; diff --git a/front/bun.lock b/front/bun.lock index 7fe6ead7..de046503 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -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=="], diff --git a/front/i18n-d.d.ts b/front/i18n-d.d.ts new file mode 100644 index 00000000..3092f6ca --- /dev/null +++ b/front/i18n-d.d.ts @@ -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 }; + } +} diff --git a/front/package.json b/front/package.json index 85e87a44..cb55cd7d 100644 --- a/front/package.json +++ b/front/package.json @@ -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", diff --git a/front/packages/ui/src/i18n-d.d.ts b/front/packages/ui/src/i18n-d.d.ts index 4fac2975..3092f6ca 100644 --- a/front/packages/ui/src/i18n-d.d.ts +++ b/front/packages/ui/src/i18n-d.d.ts @@ -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 . - */ - 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; - } } diff --git a/front/translations/am.json b/front/public/translations/am.json similarity index 100% rename from front/translations/am.json rename to front/public/translations/am.json diff --git a/front/translations/ar.json b/front/public/translations/ar.json similarity index 100% rename from front/translations/ar.json rename to front/public/translations/ar.json diff --git a/front/translations/de.json b/front/public/translations/de.json similarity index 100% rename from front/translations/de.json rename to front/public/translations/de.json diff --git a/front/translations/en.json b/front/public/translations/en.json similarity index 100% rename from front/translations/en.json rename to front/public/translations/en.json diff --git a/front/translations/es.json b/front/public/translations/es.json similarity index 100% rename from front/translations/es.json rename to front/public/translations/es.json diff --git a/front/translations/fr.json b/front/public/translations/fr.json similarity index 100% rename from front/translations/fr.json rename to front/public/translations/fr.json diff --git a/front/translations/is.json b/front/public/translations/is.json similarity index 100% rename from front/translations/is.json rename to front/public/translations/is.json diff --git a/front/translations/it.json b/front/public/translations/it.json similarity index 100% rename from front/translations/it.json rename to front/public/translations/it.json diff --git a/front/translations/ko.json b/front/public/translations/ko.json similarity index 100% rename from front/translations/ko.json rename to front/public/translations/ko.json diff --git a/front/translations/ml.json b/front/public/translations/ml.json similarity index 100% rename from front/translations/ml.json rename to front/public/translations/ml.json diff --git a/front/translations/nl.json b/front/public/translations/nl.json similarity index 100% rename from front/translations/nl.json rename to front/public/translations/nl.json diff --git a/front/translations/pl.json b/front/public/translations/pl.json similarity index 100% rename from front/translations/pl.json rename to front/public/translations/pl.json diff --git a/front/translations/pt.json b/front/public/translations/pt.json similarity index 100% rename from front/translations/pt.json rename to front/public/translations/pt.json diff --git a/front/translations/pt_br.json b/front/public/translations/pt_br.json similarity index 100% rename from front/translations/pt_br.json rename to front/public/translations/pt_br.json diff --git a/front/translations/ro.json b/front/public/translations/ro.json similarity index 100% rename from front/translations/ro.json rename to front/public/translations/ro.json diff --git a/front/translations/ru.json b/front/public/translations/ru.json similarity index 100% rename from front/translations/ru.json rename to front/public/translations/ru.json diff --git a/front/translations/ta.json b/front/public/translations/ta.json similarity index 100% rename from front/translations/ta.json rename to front/public/translations/ta.json diff --git a/front/translations/tr.json b/front/public/translations/tr.json similarity index 100% rename from front/translations/tr.json rename to front/public/translations/tr.json diff --git a/front/translations/uk.json b/front/public/translations/uk.json similarity index 100% rename from front/translations/uk.json rename to front/public/translations/uk.json diff --git a/front/translations/zh.json b/front/public/translations/zh.json similarity index 100% rename from front/translations/zh.json rename to front/public/translations/zh.json diff --git a/front/src/primitives/links.tsx b/front/src/primitives/links.tsx index 7073f19d..89327d90 100644 --- a/front/src/primitives/links.tsx +++ b/front/src/primitives/links.tsx @@ -95,7 +95,6 @@ export const Link = ({ } & PressableProps) => { const linkProps = useLinkTo({ href: href ?? "#", replace }); - console.warn(children); return ( { const [queryClient] = useState(() => createQueryClient()); @@ -33,7 +34,9 @@ export const Providers = ({ children }: { children: ReactNode }) => { - {children} + + {children} + diff --git a/front/src/providers/translations.ssr.tsx b/front/src/providers/translations.ssr.tsx new file mode 100644 index 00000000..8b6b1606 --- /dev/null +++ b/front/src/providers/translations.ssr.tsx @@ -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({ + 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 {children}; +}; diff --git a/front/src/providers/translations.tsx b/front/src/providers/translations.tsx new file mode 100644 index 00000000..19b5d748 --- /dev/null +++ b/front/src/providers/translations.tsx @@ -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({ + 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 {children}; +}; diff --git a/front/src/ui/errors/unauthorized.tsx b/front/src/ui/errors/unauthorized.tsx index 9f215766..7d6c44bd 100644 --- a/front/src/ui/errors/unauthorized.tsx +++ b/front/src/ui/errors/unauthorized.tsx @@ -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(); diff --git a/front/translations/index.js b/front/translations/index.js deleted file mode 100644 index 8dadd9a1..00000000 --- a/front/translations/index.js +++ /dev/null @@ -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 }, -};