i18n support

This commit is contained in:
Kalle 2022-07-16 14:56:20 +03:00
parent e65580685c
commit 640eba9cea
13 changed files with 419 additions and 37 deletions

View File

@ -5,6 +5,7 @@ import { Image } from "../Image";
import { useIsMounted } from "~/hooks/useIsMounted";
import { canPerformAdminActions } from "~/permissions";
import { useUser } from "~/modules/auth";
import { useTranslation } from "react-i18next";
export function Menu({
expanded,
@ -15,6 +16,7 @@ export function Menu({
}) {
const user = useUser();
const isMounted = useIsMounted();
const { t } = useTranslation();
// without this menu is initially visible due to SSR and not knowing user screen width on server (probably)
if (!isMounted) return null;
@ -47,7 +49,7 @@ export function Menu({
path={`/img/layout/${navItem.name.replace(" ", "")}`}
alt={navItem.name}
/>
<div>{navItem.displayName ?? navItem.name}</div>
<div>{t(`pages.${navItem.name}`)}</div>
</Link>
))}
</div>

View File

@ -1,13 +1,11 @@
[
{
"name": "admin",
"displayName": "Admin",
"url": "admin"
},
{ "name": "badges", "displayName": "Badges", "url": "badges" },
{ "name": "badges", "url": "badges" },
{
"name": "plus",
"displayName": "Plus Server",
"url": "plus/suggestions"
}
]

View File

@ -1,10 +1,27 @@
import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
import { i18nLoader } from "./modules/i18n";
import i18next from "i18next";
import { I18nextProvider } from "react-i18next";
i18nLoader().then(hydrate).catch(console.error);
// work around for react 18 + cypress problem - https://github.com/remix-run/remix/issues/2570#issuecomment-1099696456
if (process.env.NODE_ENV === "test") {
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("react-dom").hydrate(<RemixBrowser />, document);
} else {
hydrateRoot(document, <RemixBrowser />);
function hydrate() {
if (process.env.NODE_ENV === "test") {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require("react-dom").hydrate(
<I18nextProvider i18n={i18next}>
<RemixBrowser />
</I18nextProvider>,
document
);
} else {
return hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<RemixBrowser />
</I18nextProvider>
);
}
}

View File

@ -1,17 +1,23 @@
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { I18nextProvider } from "react-i18next";
import { renderToString } from "react-dom/server";
import cron from "node-cron";
import { updatePatreonData } from "./modules/patreon";
import { i18Instance } from "./modules/i18n";
export default function handleRequest(
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const i18n = await i18Instance(request, remixContext);
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
<I18nextProvider i18n={i18n}>
<RemixServer context={remixContext} url={request.url} />
</I18nextProvider>
);
responseHeaders.set("Content-Type", "text/html");

View File

@ -0,0 +1,8 @@
export const DEFAULT_LANGUAGE = "en";
export const config = {
supportedLngs: ["en"],
fallbackLng: DEFAULT_LANGUAGE,
defaultNS: "common",
react: { useSuspense: false },
};

View File

@ -0,0 +1,20 @@
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
import { config } from "./config";
export const i18next = new RemixI18Next({
detection: {
supportedLanguages: config.supportedLngs,
fallbackLanguage: config.fallbackLng,
},
i18next: {
...config,
backend: {
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
},
},
backend: Backend,
});
export default i18next;

View File

@ -0,0 +1,4 @@
export { i18nLoader } from "./loader";
export { i18Instance } from "./loader.server";
export { i18next } from "./i18next.server";
export { DEFAULT_LANGUAGE } from "./config";

View File

@ -0,0 +1,28 @@
import type { EntryContext } from "@remix-run/server-runtime";
import { createInstance } from "i18next";
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { initReactI18next } from "react-i18next";
import { config } from "./config";
import i18next from "./i18next.server";
export async function i18Instance(request: Request, context: EntryContext) {
const instance = createInstance();
const lng = await i18next.getLocale(request);
const ns = i18next.getRouteNamespaces(context);
await instance
.use(initReactI18next)
.use(Backend)
.init({
...config,
lng,
ns,
backend: {
loadPath: resolve("./public/locales/{{lng}}/{{ns}}.json"),
},
});
return instance;
}

View File

@ -0,0 +1,24 @@
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next";
import { config } from "./config";
export function i18nLoader() {
return i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init({
...config,
ns: getInitialNamespaces(),
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
detection: {
order: ["htmlTag"],
caches: [],
},
});
}

View File

@ -25,6 +25,9 @@ import { db } from "./db";
import type { FindAllPatrons } from "./db/models/users.server";
import type { UserWithPlusTier } from "./db/types";
import { getUser } from "./modules/auth";
import { DEFAULT_LANGUAGE, i18next } from "./modules/i18n";
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
export const unstable_shouldReload: ShouldReloadFunction = () => false;
@ -44,6 +47,7 @@ export const meta: MetaFunction = () => ({
});
export interface RootLoaderData {
locale: string;
patrons: FindAllPatrons;
user?: Pick<
UserWithPlusTier,
@ -53,8 +57,10 @@ export interface RootLoaderData {
export const loader: LoaderFunction = async ({ request }) => {
const user = await getUser(request);
const locale = await i18next.getLocale(request);
return json<RootLoaderData>({
locale,
patrons: db.users.findAllPatrons(),
user: user
? {
@ -67,17 +73,23 @@ export const loader: LoaderFunction = async ({ request }) => {
});
};
export const handle = {
i18n: "common",
};
function Document({
children,
patrons,
isCatchBoundary,
data,
}: {
children: React.ReactNode;
patrons?: RootLoaderData["patrons"];
isCatchBoundary?: boolean;
data?: RootLoaderData;
}) {
const { i18n } = useTranslation();
const locale = data?.locale ?? DEFAULT_LANGUAGE;
useChangeLanguage(locale);
return (
<html lang="en">
<html lang={locale} dir={i18n.dir()}>
<head>
<Meta />
<meta name="color-scheme" content="dark light" />
@ -85,7 +97,7 @@ function Document({
</head>
<body>
<React.StrictMode>
<Layout patrons={patrons} isCatchBoundary={isCatchBoundary}>
<Layout patrons={data?.patrons} isCatchBoundary={!data}>
{children}
</Layout>
</React.StrictMode>
@ -98,12 +110,12 @@ function Document({
}
export default function App() {
// prop drilling patrons instead of using useLoaderData in the Footer directly because
// useLoaderData can't be used in CatchBoundary and Footer is rendered in it as well
// prop drilling data instead of using useLoaderData in the child components directly because
// useLoaderData can't be used in CatchBoundary and layout is rendered in it as well
const data = useLoaderData<RootLoaderData>();
return (
<Document patrons={data.patrons}>
<Document data={data}>
<Outlet />
</Document>
);
@ -111,7 +123,7 @@ export default function App() {
export function CatchBoundary() {
return (
<Document isCatchBoundary>
<Document>
<Catcher />
</Document>
);

283
package-lock.json generated
View File

@ -17,13 +17,19 @@
"countries-list": "^2.6.1",
"date-fns": "^2.28.0",
"fuse.js": "^6.6.2",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-fs-backend": "^1.1.4",
"i18next-http-backend": "^1.4.1",
"just-shuffle": "^4.0.1",
"node-cron": "3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^11.18.1",
"react-popper": "^2.3.0",
"remix-auth": "^3.2.2",
"remix-auth-oauth2": "^1.2.2",
"remix-i18next": "^4.1.1",
"tiny-invariant": "^1.2.0",
"zod": "^3.17.3"
},
@ -31,6 +37,7 @@
"@remix-run/dev": "^1.6.5",
"@remix-run/eslint-config": "^1.6.5",
"@types/better-sqlite3": "^7.5.0",
"@types/i18next-fs-backend": "^1.1.2",
"@types/node-cron": "^3.0.2",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
@ -2863,6 +2870,15 @@
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
"dev": true
},
"node_modules/@types/i18next-fs-backend": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/i18next-fs-backend/-/i18next-fs-backend-1.1.2.tgz",
"integrity": "sha512-ZzTRXA5B0x0oGhzKNp08IsYjZpli4LjRZpg3q4j0XFxN5lKG2MVLnR4yHX8PPExBk4sj9Yfk1z9O6CjPrAlmIQ==",
"dev": true,
"dependencies": {
"i18next": "^21.0.1"
}
},
"node_modules/@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@ -3364,6 +3380,11 @@
"node": ">=6.5"
}
},
"node_modules/accept-language-parser": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
"integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw=="
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -5056,6 +5077,14 @@
"yarn": ">=1"
}
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -7446,8 +7475,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.2.11",
@ -8324,6 +8352,14 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-tags": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz",
@ -8396,6 +8432,49 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/i18next": {
"version": "21.8.14",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.14.tgz",
"integrity": "sha512-4Yi+DtexvMm/Yw3Q9fllzY12SgLk+Mcmar+rCAccsOPul/2UmnBzoHbTGn/L48IPkFcmrNaH7xTLboBWIbH6pw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.17.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.4.tgz",
"integrity": "sha512-wukWnFeU7rKIWT66VU5i8I+3Zc4wReGcuDK2+kuFhtoxBRGWGdvYI9UQmqNL/yQH1KogWwh+xGEaIPH8V/i2Zg==",
"dependencies": {
"@babel/runtime": "^7.14.6"
}
},
"node_modules/i18next-fs-backend": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-1.1.4.tgz",
"integrity": "sha512-/MfAGMP0jHonV966uFf9PkWWuDjPYLIcsipnSO3NxpNtAgRUKLTwvm85fEmsF6hGeu0zbZiCQ3W74jwO6K9uXA=="
},
"node_modules/i18next-http-backend": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz",
"integrity": "sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==",
"dependencies": {
"cross-fetch": "3.1.5"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -8549,6 +8628,14 @@
"node": ">= 0.4"
}
},
"node_modules/intl-parse-accept-language": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/intl-parse-accept-language/-/intl-parse-accept-language-1.0.0.tgz",
"integrity": "sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA==",
"engines": {
"node": ">=14"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -11061,7 +11148,6 @@
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@ -12109,6 +12195,27 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"node_modules/react-i18next": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz",
"integrity": "sha512-S8cl4mvIOSA7OQCE5jNy2yhv705Vwi+7PinpqKIYcBmX/trJtHKqrf6CL67WJSA8crr2JU+oxE9jn9DQIrQezg==",
"dependencies": {
"@babel/runtime": "^7.14.5",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 19.0.0",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -12543,6 +12650,32 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/remix-i18next": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/remix-i18next/-/remix-i18next-4.1.1.tgz",
"integrity": "sha512-uyHzycfyUPrqA039UGSpfC/5xUMidmxp+b8oTTl9/ghB75tLR24XuRv28eJzS1HeautJI4l/v/iC8L3cVnZK7A==",
"dependencies": {
"accept-language-parser": "^1.5.0",
"intl-parse-accept-language": "^1.0.0",
"lru-cache": "^7.10.0",
"use-consistent-value": "^1.0.0"
},
"peerDependencies": {
"@remix-run/react": "^1.0.0",
"@remix-run/server-runtime": "^1.0.0",
"i18next": "^21.3.3",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-i18next": "^11.13.0"
}
},
"node_modules/remix-i18next/node_modules/lru-cache": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz",
"integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/repeat-element": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
@ -14252,8 +14385,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"node_modules/trim-newlines": {
"version": "3.0.1",
@ -14809,6 +14941,20 @@
"node": ">=0.10.0"
}
},
"node_modules/use-consistent-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/use-consistent-value/-/use-consistent-value-1.0.0.tgz",
"integrity": "sha512-enIOysu0IwqjafsUXvhZPajB6UYjxwu8w38xWaHLfVs1onIyg2c0DwPgcknxqO0TpYpAHXdFDXOp7Cs9HbTIkw==",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/util": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
@ -14978,6 +15124,14 @@
"node": ">=4"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@ -15017,14 +15171,12 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
"dev": true
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@ -17335,6 +17487,15 @@
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
"dev": true
},
"@types/i18next-fs-backend": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@types/i18next-fs-backend/-/i18next-fs-backend-1.1.2.tgz",
"integrity": "sha512-ZzTRXA5B0x0oGhzKNp08IsYjZpli4LjRZpg3q4j0XFxN5lKG2MVLnR4yHX8PPExBk4sj9Yfk1z9O6CjPrAlmIQ==",
"dev": true,
"requires": {
"i18next": "^21.0.1"
}
},
"@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@ -17709,6 +17870,11 @@
"event-target-shim": "^5.0.0"
}
},
"accept-language-parser": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
"integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw=="
},
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -18964,6 +19130,14 @@
"cross-spawn": "^7.0.1"
}
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -20683,8 +20857,7 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-glob": {
"version": "3.2.11",
@ -21358,6 +21531,14 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"requires": {
"void-elements": "3.1.0"
}
},
"html-tags": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz",
@ -21411,6 +21592,35 @@
}
}
},
"i18next": {
"version": "21.8.14",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.14.tgz",
"integrity": "sha512-4Yi+DtexvMm/Yw3Q9fllzY12SgLk+Mcmar+rCAccsOPul/2UmnBzoHbTGn/L48IPkFcmrNaH7xTLboBWIbH6pw==",
"requires": {
"@babel/runtime": "^7.17.2"
}
},
"i18next-browser-languagedetector": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.4.tgz",
"integrity": "sha512-wukWnFeU7rKIWT66VU5i8I+3Zc4wReGcuDK2+kuFhtoxBRGWGdvYI9UQmqNL/yQH1KogWwh+xGEaIPH8V/i2Zg==",
"requires": {
"@babel/runtime": "^7.14.6"
}
},
"i18next-fs-backend": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-1.1.4.tgz",
"integrity": "sha512-/MfAGMP0jHonV966uFf9PkWWuDjPYLIcsipnSO3NxpNtAgRUKLTwvm85fEmsF6hGeu0zbZiCQ3W74jwO6K9uXA=="
},
"i18next-http-backend": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.4.1.tgz",
"integrity": "sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==",
"requires": {
"cross-fetch": "3.1.5"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -21523,6 +21733,11 @@
"side-channel": "^1.0.4"
}
},
"intl-parse-accept-language": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/intl-parse-accept-language/-/intl-parse-accept-language-1.0.0.tgz",
"integrity": "sha512-YFMSV91JNBOSjw1cOfw2tup6hDP7mkz+2AUV7W1L1AM6ntgI75qC1ZeFpjPGMrWp+upmBRTX2fJWQ8c7jsUWpA=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -23304,7 +23519,6 @@
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
@ -24074,6 +24288,15 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-i18next": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz",
"integrity": "sha512-S8cl4mvIOSA7OQCE5jNy2yhv705Vwi+7PinpqKIYcBmX/trJtHKqrf6CL67WJSA8crr2JU+oxE9jn9DQIrQezg==",
"requires": {
"@babel/runtime": "^7.14.5",
"html-parse-stringify": "^3.0.1"
}
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -24412,6 +24635,24 @@
}
}
},
"remix-i18next": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/remix-i18next/-/remix-i18next-4.1.1.tgz",
"integrity": "sha512-uyHzycfyUPrqA039UGSpfC/5xUMidmxp+b8oTTl9/ghB75tLR24XuRv28eJzS1HeautJI4l/v/iC8L3cVnZK7A==",
"requires": {
"accept-language-parser": "^1.5.0",
"intl-parse-accept-language": "^1.0.0",
"lru-cache": "^7.10.0",
"use-consistent-value": "^1.0.0"
},
"dependencies": {
"lru-cache": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.13.1.tgz",
"integrity": "sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ=="
}
}
},
"repeat-element": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz",
@ -25740,8 +25981,7 @@
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"trim-newlines": {
"version": "3.0.1",
@ -26134,6 +26374,14 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
"use-consistent-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/use-consistent-value/-/use-consistent-value-1.0.0.tgz",
"integrity": "sha512-enIOysu0IwqjafsUXvhZPajB6UYjxwu8w38xWaHLfVs1onIyg2c0DwPgcknxqO0TpYpAHXdFDXOp7Cs9HbTIkw==",
"requires": {
"fast-deep-equal": "^3.1.3"
}
},
"util": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
@ -26260,6 +26508,11 @@
"unist-util-stringify-position": "^3.0.0"
}
},
"void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
@ -26294,14 +26547,12 @@
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
"dev": true
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
"dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"

View File

@ -38,13 +38,19 @@
"countries-list": "^2.6.1",
"date-fns": "^2.28.0",
"fuse.js": "^6.6.2",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-fs-backend": "^1.1.4",
"i18next-http-backend": "^1.4.1",
"just-shuffle": "^4.0.1",
"node-cron": "3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^11.18.1",
"react-popper": "^2.3.0",
"remix-auth": "^3.2.2",
"remix-auth-oauth2": "^1.2.2",
"remix-i18next": "^4.1.1",
"tiny-invariant": "^1.2.0",
"zod": "^3.17.3"
},
@ -52,6 +58,7 @@
"@remix-run/dev": "^1.6.5",
"@remix-run/eslint-config": "^1.6.5",
"@types/better-sqlite3": "^7.5.0",
"@types/i18next-fs-backend": "^1.1.2",
"@types/node-cron": "^3.0.2",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",

View File

@ -0,0 +1,5 @@
{
"pages.admin": "Admin",
"pages.badges": "Badges",
"pages.plus": "Plus Server"
}