diff --git a/app/routes/badges/$id.tsx b/app/routes/badges/$id.tsx index 6d22f0ae3..fdbab8e51 100644 --- a/app/routes/badges/$id.tsx +++ b/app/routes/badges/$id.tsx @@ -6,16 +6,17 @@ import { Badge } from "~/components/Badge"; import { LinkButton } from "~/components/Button"; import { Redirect } from "~/components/Redirect"; import { db } from "~/db"; -import type { - ManagersByBadgeId, - OwnersByBadgeId, +import { + type ManagersByBadgeId, + type OwnersByBadgeId, } from "~/db/models/badges.server"; -import type { Badge as BadgeDBType } from "~/db/types"; +import { type Badge as BadgeDBType } from "~/db/types"; import { useUser } from "~/modules/auth"; import { canEditBadgeOwners } from "~/permissions"; import { discordFullName } from "~/utils/strings"; import { BADGES_PAGE } from "~/utils/urls"; -import type { BadgesLoaderData } from "../badges"; +import { type BadgesLoaderData } from "../badges"; +import { useTranslation, type TFunction } from "react-i18next"; export interface BadgeDetailsContext { badgeName: string; @@ -44,6 +45,7 @@ export default function BadgeDetailsPage() { const { badges } = parentRoute!.data as BadgesLoaderData; const params = useParams(); const data = useLoaderData(); + const { t } = useTranslation("badges"); const badge = badges.find((b) => b.id === Number(params["id"])); if (!badge) return ; @@ -55,7 +57,9 @@ export default function BadgeDetailsPage() {
-
{badgeExplanationText(badge)}
+
+ {badgeExplanationText(t, badge)} +
, badge: Pick & { count?: number } ) { - if (badge.code === "patreon") return "Supporter of sendou.ink on Patreon"; + if (badge.code === "patreon") return t("patreon"); if (badge.code === "patreon_plus") { - return "Supporter+ of sendou.ink on Patreon"; + return t("patreon+"); } - const countString = - badge.count && badge.count > 1 ? ` (x${badge.count})` : ""; - return `Awarded for winning ${badge.displayName}${countString}`; + return t("tournament", { count: badge.count, tournament: badge.displayName }); } diff --git a/app/routes/u.$identifier.tsx b/app/routes/u.$identifier.tsx index 9ab0274e5..b98b67ef1 100644 --- a/app/routes/u.$identifier.tsx +++ b/app/routes/u.$identifier.tsx @@ -2,6 +2,7 @@ import type { LoaderFunction, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Outlet, useLoaderData } from "@remix-run/react"; import { countries } from "countries-list"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; import { Main } from "~/components/Main"; import { SubNav, SubNavLink } from "~/components/SubNav"; @@ -9,6 +10,8 @@ import { db } from "~/db"; import type { CountsByUserId } from "~/db/models/badges.server"; import type { User } from "~/db/types"; import { useUser } from "~/modules/auth"; +import { i18next } from "~/modules/i18n"; +import { translatedCountry } from "~/utils/i18n.server"; import { notFoundIfFalsy } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { discordFullName } from "~/utils/strings"; @@ -19,6 +22,10 @@ export const meta: MetaFunction = ({ data }: { data: UserPageLoaderData }) => { }; }; +export const handle = { + i18n: "user", +}; + export const userParamsSchema = z.object({ identifier: z.string() }); export type UserPageLoaderData = Pick< @@ -33,11 +40,12 @@ export type UserPageLoaderData = Pick< | "twitter" | "bio" > & { - country?: { name: string; emoji: string; code: string }; + country?: { emoji: string; code: string; name: string }; badges: CountsByUserId; }; -export const loader: LoaderFunction = ({ params }) => { +export const loader: LoaderFunction = async ({ request, params }) => { + const locale = await i18next.getLocale(request); const { identifier } = userParamsSchema.parse(params); const user = notFoundIfFalsy(db.users.findByIdentifier(identifier)); @@ -58,9 +66,13 @@ export const loader: LoaderFunction = ({ params }) => { country: countryObj && user.country ? { - name: countryObj.name, emoji: countryObj.emoji, code: user.country, + name: + translatedCountry({ + language: locale, + countryCode: user.country, + }) ?? countryObj.name, } : undefined, badges: db.badges.countsByUserId(user.id), @@ -70,6 +82,7 @@ export const loader: LoaderFunction = ({ params }) => { export default function UserPageLayout() { const data = useLoaderData(); const user = useUser(); + const { t } = useTranslation(); const isOwnPage = data.id === user?.id; @@ -77,11 +90,11 @@ export default function UserPageLayout() { <> - Profile + {t("header.profile")} {isOwnPage ? ( - Edit + {t("actions.edit")} ) : null} diff --git a/app/routes/u.$identifier/edit.tsx b/app/routes/u.$identifier/edit.tsx index 723b44d6f..d02a4fcea 100644 --- a/app/routes/u.$identifier/edit.tsx +++ b/app/routes/u.$identifier/edit.tsx @@ -1,19 +1,31 @@ -import type { ActionFunction, LinksFunction } from "@remix-run/node"; -import { Form, useMatches, useTransition } from "@remix-run/react"; +import { + type ActionFunction, + type LinksFunction, + type LoaderArgs, +} from "@remix-run/node"; +import { + Form, + useLoaderData, + useMatches, + useTransition, +} from "@remix-run/react"; import { countries } from "countries-list"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import invariant from "tiny-invariant"; import { z } from "zod"; import { Button } from "~/components/Button"; import { Label } from "~/components/Label"; import { USER_BIO_MAX_LENGTH } from "~/constants"; import { db } from "~/db"; -import type { User } from "~/db/types"; +import { type User } from "~/db/types"; import { requireUser } from "~/modules/auth"; +import { i18next } from "~/modules/i18n"; import styles from "~/styles/u-edit.css"; +import { translatedCountry } from "~/utils/i18n.server"; import { parseRequestFormData } from "~/utils/remix"; import { falsyToNull } from "~/utils/zod"; -import type { UserPageLoaderData } from "../u.$identifier"; +import { type UserPageLoaderData } from "../u.$identifier"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -47,45 +59,66 @@ export const action: ActionFunction = async ({ request }) => { return null; }; +export const loader = async ({ request }: LoaderArgs) => { + const locale = await i18next.getLocale(request); + + return { + countries: Object.entries(countries) + .map(([code, country]) => ({ + code, + emoji: country.emoji, + name: + translatedCountry({ + countryCode: code, + language: locale, + }) ?? country.name, + })) + .sort((a, b) => a.name.localeCompare(b.name)), + }; +}; + export default function UserEditPage() { + const data = useLoaderData(); + const { t } = useTranslation(["common", "user"]); const [, parentRoute] = useMatches(); invariant(parentRoute); - const data = parentRoute.data as UserPageLoaderData; + const parentRouteData = parentRoute.data as UserPageLoaderData; const transition = useTransition(); return (
- +
- + ); } function BioTextarea({ initialValue }: { initialValue: User["bio"] }) { + const { t } = useTranslation("user"); const [value, setValue] = React.useState(initialValue ?? ""); return ( @@ -94,7 +127,7 @@ function BioTextarea({ initialValue }: { initialValue: User["bio"] }) { htmlFor="bio" valueLimits={{ current: value.length, max: USER_BIO_MAX_LENGTH }} > - Bio + {t("bio")}