Wrap useTranslation to detect missing translations on the route

This commit is contained in:
Remmy Cat Stock 2022-11-05 02:02:35 +01:00 committed by Remmy Cat Stock
parent a6f62fa7b3
commit 2fa18547b0
39 changed files with 113 additions and 37 deletions

View File

@ -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: {

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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]);

View File

@ -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,

View File

@ -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 {

View File

@ -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";

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View 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);
}

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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(

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";