mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-08 02:34:16 -04:00
Add translation provider with http & fs backend
This commit is contained in:
parent
38033307b8
commit
fe816aa276
@ -1,3 +1,5 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { type LibraryItem, LibraryItemP } from "~/models";
|
import { type LibraryItem, LibraryItemP } from "~/models";
|
||||||
import { P } from "~/primitives";
|
import { P } from "~/primitives";
|
||||||
@ -9,13 +11,17 @@ export async function loader() {
|
|||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<View>
|
||||||
|
<P>{t("home.recommended")}</P>
|
||||||
<Fetch
|
<Fetch
|
||||||
query={Header.query()}
|
query={Header.query()}
|
||||||
Render={({ name }) => <P {...css({ bg: "red" })}>{name}</P>}
|
Render={({ name }) => <P {...css({ bg: "red" })}>{name}</P>}
|
||||||
Loader={() => <P>Loading</P>}
|
Loader={() => <P>Loading</P>}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,22 @@
|
|||||||
import { createMiddleware, setServerData } from "one";
|
import { createMiddleware, setServerData } from "one";
|
||||||
|
import { supportedLanguages } from "~/providers/translations.ssr";
|
||||||
|
|
||||||
export default createMiddleware(({ request, next }) => {
|
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") ?? "");
|
setServerData("cookies", request.headers.get("Cookies") ?? "");
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
5
front/app/translations+api.ts
Normal file
5
front/app/translations+api.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { supportedLanguages } from "~/providers/translations.ssr";
|
||||||
|
|
||||||
|
export default (): Response => {
|
||||||
|
return Response.json(supportedLanguages);
|
||||||
|
};
|
@ -11,6 +11,8 @@
|
|||||||
"expo": "~52.0.32",
|
"expo": "~52.0.32",
|
||||||
"expo-build-properties": "^0.13.2",
|
"expo-build-properties": "^0.13.2",
|
||||||
"expo-localization": "^16.0.1",
|
"expo-localization": "^16.0.1",
|
||||||
|
"i18next-fs-backend": "^2.6.0",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
"jotai": "^2.11.3",
|
"jotai": "^2.11.3",
|
||||||
"one": "1.1.437",
|
"one": "1.1.437",
|
||||||
"react": "^19.0.0",
|
"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=="],
|
"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=="],
|
"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": ["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=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
9
front/i18n-d.d.ts
vendored
Normal file
9
front/i18n-d.d.ts
vendored
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,8 @@
|
|||||||
"expo": "~52.0.32",
|
"expo": "~52.0.32",
|
||||||
"expo-build-properties": "^0.13.2",
|
"expo-build-properties": "^0.13.2",
|
||||||
"expo-localization": "^16.0.1",
|
"expo-localization": "^16.0.1",
|
||||||
|
"i18next-fs-backend": "^2.6.0",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
"jotai": "^2.11.3",
|
"jotai": "^2.11.3",
|
||||||
"one": "1.1.437",
|
"one": "1.1.437",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
26
front/packages/ui/src/i18n-d.d.ts
vendored
26
front/packages/ui/src/i18n-d.d.ts
vendored
@ -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 "i18next";
|
||||||
import type en from "../../../translations/en.json";
|
import type en from "./public/translations/en.json";
|
||||||
|
|
||||||
declare module "i18next" {
|
declare module "i18next" {
|
||||||
interface CustomTypeOptions {
|
interface CustomTypeOptions {
|
||||||
returnNull: false;
|
returnNull: false;
|
||||||
resources: { translation: typeof en };
|
resources: { translation: typeof en };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface i18n {
|
|
||||||
systemLanguage: string;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,6 @@ export const Link = ({
|
|||||||
} & PressableProps) => {
|
} & PressableProps) => {
|
||||||
const linkProps = useLinkTo({ href: href ?? "#", replace });
|
const linkProps = useLinkTo({ href: href ?? "#", replace });
|
||||||
|
|
||||||
console.warn(children);
|
|
||||||
return (
|
return (
|
||||||
<PressableFeedback
|
<PressableFeedback
|
||||||
{...linkProps}
|
{...linkProps}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { HydrationBoundary, QueryClientProvider } from "@tanstack/react-query";
|
import { HydrationBoundary, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { getServerData } from "one";
|
import { getServerData } from "one";
|
||||||
import { type ReactNode, useState } from "react";
|
import { type ReactNode, useMemo, useState } from "react";
|
||||||
// import { useUserTheme } from "@kyoo/models";
|
// import { useUserTheme } from "@kyoo/models";
|
||||||
import { ThemeSelector } from "~/primitives/theme";
|
import { ThemeSelector } from "~/primitives/theme";
|
||||||
import { createQueryClient } from "~/query";
|
import { createQueryClient } from "~/query";
|
||||||
import { AccountProvider } from "./account-provider";
|
import { AccountProvider } from "./account-provider";
|
||||||
import { ErrorConsumer, ErrorProvider } from "./error-provider";
|
import { ErrorConsumer, ErrorProvider } from "./error-provider";
|
||||||
|
import { TranslationsProvider } from "./translations.ssr";
|
||||||
|
|
||||||
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
@ -33,7 +34,9 @@ export const Providers = ({ children }: { children: ReactNode }) => {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ErrorProvider>
|
<ErrorProvider>
|
||||||
<AccountProvider>
|
<AccountProvider>
|
||||||
|
<TranslationsProvider>
|
||||||
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
<ErrorConsumer scope="root">{children}</ErrorConsumer>
|
||||||
|
</TranslationsProvider>
|
||||||
</AccountProvider>
|
</AccountProvider>
|
||||||
</ErrorProvider>
|
</ErrorProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
34
front/src/providers/translations.ssr.tsx
Normal file
34
front/src/providers/translations.ssr.tsx
Normal 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>;
|
||||||
|
};
|
26
front/src/providers/translations.tsx
Normal file
26
front/src/providers/translations.tsx
Normal 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>;
|
||||||
|
};
|
@ -5,8 +5,6 @@ import { useYoshiki } from "yoshiki/native";
|
|||||||
import { Button, Icon, Link, P, ts } from "~/primitives";
|
import { Button, Icon, Link, P, ts } from "~/primitives";
|
||||||
import { useAccount } from "~/providers/account-provider";
|
import { useAccount } from "~/providers/account-provider";
|
||||||
|
|
||||||
console.log(Register);
|
|
||||||
|
|
||||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
@ -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 },
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user