mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 06:58:10 -05:00
Wrap useTranslation to detect missing translations on the route
This commit is contained in:
parent
a6f62fa7b3
commit
2fa18547b0
13
.eslintrc.js
13
.eslintrc.js
|
|
@ -31,6 +31,19 @@ module.exports = {
|
|||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"react/prop-types": 0,
|
||||
"@typescript-eslint/no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "react-i18next",
|
||||
importNames: ["useTranslation"],
|
||||
message:
|
||||
"Please import useTranslation from '~/hooks/useTranslation' instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Link, useMatches } from "@remix-run/react";
|
||||
import { useMemo, Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { isDefined } from "~/utils/arrays";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { Build, BuildWeapon, GearType, User } from "~/db/types";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import clsx from "clsx";
|
|||
import type { Unpacked } from "~/utils/types";
|
||||
import type { GearType, UserWithPlusTier } from "~/db/types";
|
||||
import { useAllEventsWithMapPools, useUsers } from "~/hooks/swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import {
|
||||
clothesGearIds,
|
||||
headGearIds,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useActionData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
||||
export function FormErrors({ namespace }: { namespace: "user" | "calendar" }) {
|
||||
const { t } = useTranslation(["common", namespace]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Image } from "~/components/Image";
|
||||
import {
|
||||
type ModeShort,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { languages } from "~/modules/i18n";
|
||||
import { LinkButton } from "../Button";
|
||||
import { GlobeIcon } from "../icons/Globe";
|
||||
|
|
|
|||
|
|
@ -5,7 +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";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
||||
export function Menu({
|
||||
expanded,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Theme, useTheme } from "~/modules/theme";
|
||||
import { Button } from "../Button";
|
||||
import { MoonIcon } from "../icons/Moon";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Link, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import { LOG_IN_URL, LOG_OUT_URL, userPage } from "~/utils/urls";
|
||||
import { Avatar } from "../Avatar";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Link, useMatches } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { LOGO_PATH, navIconUrl } from "~/utils/urls";
|
||||
|
|
|
|||
59
app/hooks/useTranslation.ts
Normal file
59
app/hooks/useTranslation.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from "react";
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import {
|
||||
type DefaultNamespace,
|
||||
type KeyPrefix,
|
||||
type Namespace,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
useTranslation as useTranslationOriginal,
|
||||
type UseTranslationOptions,
|
||||
type UseTranslationResponse,
|
||||
} from "react-i18next";
|
||||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
// Wraps useTranslation for better error detection with remix-i18next.
|
||||
// Only has an effect in non-production environments.
|
||||
export function useTranslation<
|
||||
N extends Namespace = DefaultNamespace,
|
||||
TKPrefix extends KeyPrefix<N> = undefined
|
||||
>(
|
||||
ns?: N | Readonly<N>,
|
||||
options?: UseTranslationOptions<TKPrefix>
|
||||
): UseTranslationResponse<N, TKPrefix> {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// These are safe because the condition cannot change at runtime, so the
|
||||
// dev build will _always_ call these hooks, and the prod build _never_.
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const matches = useMatches();
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const loadedTranslations: Set<string> = React.useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
matches.flatMap((m) => (m.handle as SendouRouteHandle)?.i18n ?? [])
|
||||
),
|
||||
[matches]
|
||||
);
|
||||
|
||||
// Reset the typing to the actual representation to be able to read from it.
|
||||
const nsFixed = ns as string | string[] | undefined;
|
||||
const nsArray =
|
||||
nsFixed === undefined
|
||||
? ["common"]
|
||||
: typeof nsFixed === "string"
|
||||
? [nsFixed]
|
||||
: nsFixed;
|
||||
|
||||
for (const singleNs of nsArray) {
|
||||
if (!loadedTranslations.has(singleNs)) {
|
||||
throw new Error(
|
||||
`Tried to access translation file "${singleNs}", but the active routes only configured need for these files: ${JSON.stringify(
|
||||
[...loadedTranslations.values()]
|
||||
)}.\nForgot to add "${singleNs}" translation to SendouRouteHandle (handle.i18n)?`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return useTranslationOriginal<N, TKPrefix>(ns, options);
|
||||
}
|
||||
|
|
@ -28,7 +28,8 @@ import type { UserWithPlusTier } from "./db/types";
|
|||
import { getUser } from "./modules/auth";
|
||||
import { DEFAULT_LANGUAGE, i18nCookie, i18next } from "./modules/i18n";
|
||||
import { useChangeLanguage } from "remix-i18next";
|
||||
import { type CustomTypeOptions, useTranslation } from "react-i18next";
|
||||
import { type CustomTypeOptions } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Theme, ThemeHead, useTheme, ThemeProvider } from "./modules/theme";
|
||||
import { getThemeSession } from "./modules/theme/session.server";
|
||||
import { COMMON_PREVIEW_IMAGE } from "./utils/urls";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { json } from "@remix-run/node";
|
|||
import { mostRecentArticles } from "~/modules/articles";
|
||||
import styles from "~/styles/front.css";
|
||||
import { ArticlesPeek } from ".";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
const MAX_ARTICLES_COUNT = 100;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { type LinksFunction, type MetaFunction } from "@remix-run/node";
|
||||
import { Link } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { db } from "~/db";
|
|||
import type { FindAll } from "~/db/models/badges/queries.server";
|
||||
import styles from "~/styles/badges.css";
|
||||
import { BORZOIC_TWITTER, FAQ_PAGE } from "~/utils/urls";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { useAnimateListEntry } from "~/hooks/useAnimateListEntry";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import { canEditBadgeOwners } from "~/permissions";
|
|||
import { discordFullName } from "~/utils/strings";
|
||||
import { BADGES_PAGE } from "~/utils/urls";
|
||||
import { type BadgesLoaderData } from "../badges";
|
||||
import { useTranslation, type TFunction } from "react-i18next";
|
||||
import { type TFunction } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
||||
export interface BadgeDetailsContext {
|
||||
badgeName: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type LinksFunction } from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useUser } from "~/modules/auth";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
type SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { BuildCard } from "~/components/BuildCard";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { BUILDS_PAGE_BATCH_SIZE, BUILDS_PAGE_MAX_BUILDS } from "~/constants";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Image } from "~/components/Image";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { weaponCategories, weaponIdIsNotAlt } from "~/modules/in-game-lists";
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useLoaderData } from "@remix-run/react";
|
|||
import { Link } from "@remix-run/react/dist/components";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { z } from "zod";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { FormMessage } from "~/components/FormMessage";
|
|||
import { FormErrors } from "~/components/FormErrors";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import { calendarEventPage } from "~/utils/urls";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
||||
const playersSchema = z
|
||||
.array(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Badge } from "~/components/Badge";
|
||||
import { Button } from "~/components/Button";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import clsx from "clsx";
|
|||
import { addDays, addMonths, subDays, subMonths } from "date-fns";
|
||||
import React from "react";
|
||||
import { Flipped, Flipper } from "react-flip-toolkit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { z } from "zod";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "~/components/Badge";
|
||||
import { Button } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import { languages } from "~/modules/i18n";
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
UBERU_TWITTER,
|
||||
} from "~/utils/urls";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { LinksFunction, MetaFunction } from "@remix-run/node";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import styles from "~/styles/faq.css";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { json, type LinksFunction } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Link } from "react-router-dom";
|
||||
import { BuildCard } from "~/components/BuildCard";
|
||||
import { ArrowRightIcon } from "~/components/icons/ArrowRight";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { ShouldReloadFunction } from "@remix-run/react";
|
|||
import { Link } from "@remix-run/react";
|
||||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Button } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type { LinksFunction } from "@remix-run/node";
|
|||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
import type { DamageReceiver, DamageType } from "~/modules/analyzer";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import clsx from "clsx";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Ability } from "~/components/Ability";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {
|
|||
import { json } from "@remix-run/node";
|
||||
import { Outlet, useLoaderData, useLocation } from "@remix-run/react";
|
||||
import { countries } from "countries-list";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { z } from "zod";
|
||||
import { SubNav, SubNavLink } from "~/components/SubNav";
|
||||
import { db } from "~/db";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ActionFunction } from "@remix-run/node";
|
||||
import { json, type LoaderArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useMatches } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { z } from "zod";
|
||||
import { BuildCard } from "~/components/BuildCard";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "@remix-run/node";
|
||||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { z } from "zod";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Button } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from "@remix-run/react";
|
||||
import { countries } from "countries-list";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMatches } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Badge } from "~/components/Badge";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Link } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { type UserPageLoaderData } from "~/routes/u.$identifier";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type ActionFunction, redirect } from "@remix-run/node";
|
||||
import { Form, useMatches, useTransition } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/Button";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useMatches } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import invariant from "tiny-invariant";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { Main } from "~/components/Main";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user