i18n
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 10s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped

This commit is contained in:
2026-08-08 15:29:40 -07:00
parent 22c282d8fa
commit 0a7b5fa00f
178 changed files with 13099 additions and 1285 deletions
@@ -6,6 +6,7 @@ import MatchDock from "./match-dock";
import useAppShellHeight from "@/hooks/use-appshell-height";
import { Match } from "@/features/matches/types";
import styles from "./styles.module.css";
import { Trans } from "@lingui/react/macro";
interface BracketViewProps {
bracket: BracketData;
@@ -95,14 +96,14 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls, groupC
>
<div>
<Text fw={600} size="md" m={16}>
Winners Bracket
<Trans>Winners Bracket</Trans>
</Text>
<Bracket rounds={bracket.winners} orders={orders} showControls={showControls} groupConfig={groupConfig} renderMatch={renderMatch} onMatchTap={handleMatchTap} selectedMatchLid={selectedLid} nextUpMatchId={nextUpMatchId} />
</div>
{bracket.losers && bracket.losers.length > 0 && bracket.losers.some(round => round.length > 0) && (
<div>
<Text fw={600} size="md" m={16}>
Losers Bracket
<Trans>Losers Bracket</Trans>
</Text>
<Bracket rounds={bracket.losers} orders={orders} showControls={showControls} groupConfig={groupConfig} renderMatch={renderMatch} onMatchTap={handleMatchTap} selectedMatchLid={selectedLid} nextUpMatchId={nextUpMatchId} />
</div>
+21 -19
View File
@@ -16,6 +16,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useSpotifyPlayback } from "@/lib/spotify/hooks";
import { getGroupLabel } from "../utils/group-label";
import styles from "./styles.module.css";
import { Trans, useLingui } from "@lingui/react/macro";
interface MatchCardProps {
match: Match;
@@ -42,6 +43,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
const queryClient = useQueryClient();
const editSheet = useSheet();
const { playTrack, pause } = useSpotifyPlayback();
const { t, i18n } = useLingui();
const canTap = !!(onTap && match.home && match.away);
@@ -59,9 +61,9 @@ export const MatchCard: React.FC<MatchCardProps> = ({
match.home_cups !== undefined &&
match.away_cups !== undefined &&
match.home_cups > match.away_cups,
groupLabel: !match.home && match.home_seed ? getGroupLabel(match.home_seed, groupConfig) : undefined,
groupLabel: !match.home && match.home_seed ? getGroupLabel(i18n, match.home_seed, groupConfig) : undefined,
}),
[match, orders, groupConfig]
[match, orders, groupConfig, i18n]
);
const awaySlot = useMemo(
() => ({
@@ -75,9 +77,9 @@ export const MatchCard: React.FC<MatchCardProps> = ({
match.away_cups !== undefined &&
match.home_cups !== undefined &&
match.away_cups > match.home_cups,
groupLabel: !match.away && match.away_seed ? getGroupLabel(match.away_seed, groupConfig) : undefined,
groupLabel: !match.away && match.away_seed ? getGroupLabel(i18n, match.away_seed, groupConfig) : undefined,
}),
[match, orders, groupConfig]
[match, orders, groupConfig, i18n]
);
const showToolbar = useMemo(
@@ -95,7 +97,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
const start = useServerMutation({
mutationFn: startMatch,
successMessage: "Match started!",
successMessage: t`Match started!`,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: tournamentKeys.details(match.tournament.id),
@@ -112,12 +114,12 @@ export const MatchCard: React.FC<MatchCardProps> = ({
editSheet.close();
if (data?.downstreamReset) {
toast.error(
"Downstream matches were reset because the correction changed who advances."
t`Downstream matches were reset because the correction changed who advances.`
);
}
if (data?.groupEditAfterKnockout) {
toast.error(
"Group result changed after the bracket was seeded — reseed the knockout stage if needed."
t`Group result changed after the bracket was seeded — reseed the knockout stage if needed.`
);
}
},
@@ -186,7 +188,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
const handleSpeakerClick = useCallback(async () => {
if (!hasWalkoutData || !match.home?.name || !match.away?.name) {
await speak(`${match.home?.name || "Home"} vs. ${match.away?.name || "Away"}`);
await speak(t`${match.home?.name || t({ message: "Home", context: "match team" })} vs. ${match.away?.name || t({ message: "Away", context: "match team" })}`);
return;
}
@@ -196,14 +198,14 @@ export const MatchCard: React.FC<MatchCardProps> = ({
await playTeamWalkout(homeTeam);
await speak(homeTeam.name);
await speak("versus");
await speak(t`versus`);
await playTeamWalkout(awayTeam);
await speak(awayTeam.name);
await speak("have fun, good luck!");
await speak(t`have fun, good luck!`);
} catch (error) {
console.warn('Walkout sequence error:', error);
await speak(`${match.home.name} vs. ${match.away.name}`);
await speak(t`${match.home.name} vs. ${match.away.name}`);
}
}, [hasWalkoutData, match.home, match.away, speak, playTeamWalkout]);
@@ -221,10 +223,10 @@ export const MatchCard: React.FC<MatchCardProps> = ({
await playTeamWalkout(homeTeam);
await speak(homeTeam.name);
await speak("versus");
await speak(t`versus`);
await playTeamWalkout(awayTeam);
await speak(awayTeam.name);
await speak("have fun, good luck!");
await speak(t`have fun, good luck!`);
} catch (error) {
console.warn('Auto-walkout sequence error:', error);
}
@@ -283,7 +285,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
}
aria-label={
canTap
? `Match ${match.order}: ${match.home!.name} vs ${match.away!.name}`
? t`Match ${match.order}: ${match.home!.name} vs ${match.away!.name}`
: undefined
}
style={{
@@ -315,7 +317,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
c="dimmed"
fw="bold"
>
* If necessary
<Trans>* If necessary</Trans>
</Text>
)}
@@ -331,7 +333,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
e.stopPropagation();
handleSpeakerClick();
}}
aria-label="Announce matchup"
aria-label={t`Announce matchup`}
>
<SpeakerHighIcon size={12} />
</ActionIcon>
@@ -349,7 +351,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
h="100%"
radius="sm"
ml={-4}
aria-label="Start match"
aria-label={t`Start match`}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
@@ -371,7 +373,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
h="100%"
radius="sm"
ml={-4}
aria-label="Edit match score"
aria-label={t`Edit match score`}
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
@@ -383,7 +385,7 @@ export const MatchCard: React.FC<MatchCardProps> = ({
)}
</Flex>
<Sheet title="Edit Match" {...editSheet.props}>
<Sheet title={t`Edit Match`} {...editSheet.props}>
<MatchForm
match={match}
onSubmit={handleFormSubmit}
+11 -6
View File
@@ -23,6 +23,7 @@ import TeamHeadToHeadSheet from "@/features/matches/components/team-head-to-head
import { MatchReport } from "./match-report";
import Sheet from "@/components/sheet/sheet";
import { useSheet } from "@/hooks/use-sheet";
import { Trans, useLingui } from "@lingui/react/macro";
const EASE: [number, number, number, number] = [0.32, 0.72, 0, 1];
@@ -81,6 +82,7 @@ const TeamRow = ({
const MatchDock = ({ match, onClose }: MatchDockProps) => {
const reduceMotion = useReducedMotion();
const h2hSheet = useSheet();
const { t } = useLingui();
const hasPrivate = match?.home?.private || match?.away?.private;
const ended = match?.status === "ended";
const started = match?.status === "started";
@@ -118,14 +120,17 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
/>
)}
<Text size="xs" fw={600} c="dimmed" lineClamp={1}>
Match {match.order} · Round {match.round + 1}
{match.is_losers_bracket && " (Losers)"}
{match.is_losers_bracket ? (
<Trans>Match {match.order} · Round {match.round + 1} (Losers)</Trans>
) : (
<Trans>Match {match.order} · Round {match.round + 1}</Trans>
)}
</Text>
</Group>
<CloseButton
size="sm"
onClick={onClose}
aria-label="Close match actions"
aria-label={t`Close match actions`}
/>
</Group>
@@ -165,12 +170,12 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
</Suspense>
</Box>
{!hasPrivate && (
<Tooltip label="Head to Head" withArrow position="top">
<Tooltip label={t`Head to Head`} withArrow position="top">
<ActionIcon
variant="subtle"
size="sm"
onClick={h2hSheet.open}
aria-label="View head-to-head"
aria-label={t`View head-to-head`}
w={40}
style={{ flexShrink: 0 }}
>
@@ -207,7 +212,7 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
</AnimatePresence>
{match?.home && match?.away && h2hSheet.isOpen && (
<Sheet title="Head to Head" {...h2hSheet.props}>
<Sheet title={t`Head to Head`} {...h2hSheet.props}>
<TeamHeadToHeadSheet
team1={match.home}
team2={match.away}
+22 -20
View File
@@ -1,6 +1,7 @@
import { Button, TextInput, Stack, Group, Text, Flex, Divider, NumberInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { Match } from "@/features/matches/types";
import { Trans, useLingui } from "@lingui/react/macro";
interface MatchFormProps {
match: Match;
@@ -19,6 +20,7 @@ export const MatchForm: React.FC<MatchFormProps> = ({
onCancel,
loading = false,
}) => {
const { t } = useLingui();
const form = useForm({
initialValues: {
home_cups: match.home_cups || 10,
@@ -27,42 +29,42 @@ export const MatchForm: React.FC<MatchFormProps> = ({
},
validate: {
home_cups: (value, values) => {
if (value === null || value === undefined) return "Home cups is required";
if (value === null || value === undefined) return t`Home cups is required`;
if (values.ot_count > 0) return null;
const homeCups = Number(value);
const awayCups = Number(values.away_cups);
if (homeCups !== 10 && awayCups !== 10) {
return "At least one team must have 10 cups";
return t`At least one team must have 10 cups`;
}
if (homeCups === 10 && awayCups === 10) {
return "Both teams cannot have 10 cups";
return t`Both teams cannot have 10 cups`;
}
return null;
},
away_cups: (value, values) => {
if (value === null || value === undefined) return "Away cups is required";
if (value === null || value === undefined) return t`Away cups is required`;
if (values.ot_count > 0) return null;
const awayCups = Number(value);
const homeCups = Number(values.home_cups);
if (homeCups !== 10 && awayCups !== 10) {
return "At least one team must have 10 cups";
return t`At least one team must have 10 cups`;
}
if (homeCups === 10 && awayCups === 10) {
return "Both teams cannot have 10 cups";
return t`Both teams cannot have 10 cups`;
}
return null;
},
ot_count: (value) =>
value === null || value === undefined
? "Overtime count is required"
? t`Overtime count is required`
: null,
},
transformValues: (values) => ({
@@ -85,7 +87,7 @@ export const MatchForm: React.FC<MatchFormProps> = ({
<Group gap="xs">
<Stack gap={0}>
<Text fw={500} size="sm">
{match.home?.name} Cups
<Trans>{match.home?.name} Cups</Trans>
</Text>
{
match.home?.players?.map(p => (<Text key={p.id} size='xs' c='dimmed'>
@@ -108,7 +110,7 @@ export const MatchForm: React.FC<MatchFormProps> = ({
<Group gap="xs">
<Stack gap={0}>
<Text fw={500} size="sm">
{match.away?.name} Cups
<Trans>{match.away?.name} Cups</Trans>
</Text>
{
match.away?.players?.map(p => (<Text key={p.id} size='xs' c='dimmed'>
@@ -130,7 +132,7 @@ export const MatchForm: React.FC<MatchFormProps> = ({
<Group gap="xs">
<Text fw={500} size="sm">
OT Count
<Trans>OT Count</Trans>
</Text>
<TextInput
ml='auto'
@@ -147,7 +149,7 @@ export const MatchForm: React.FC<MatchFormProps> = ({
<Stack mt="md">
<Button type="submit" loading={loading}>
Update Match
<Trans>Update Match</Trans>
</Button>
<Button
variant="subtle"
@@ -155,7 +157,7 @@ export const MatchForm: React.FC<MatchFormProps> = ({
onClick={onCancel}
disabled={loading}
>
Cancel
<Trans>Cancel</Trans>
</Button>
</Stack>
</Stack>
@@ -12,6 +12,7 @@ import { useSheet } from "@/hooks/use-sheet";
import Sheet from "@/components/sheet/sheet";
import { MatchForm } from "./match-form";
import toast from "@/lib/sonner";
import { Trans, useLingui } from "@lingui/react/macro";
const teamId = (t: Match["home"]): string | undefined =>
!t ? undefined : typeof t === "string" ? t : t.id;
@@ -30,6 +31,7 @@ interface MatchReportProps {
export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
const { user } = useAuth();
const formSheet = useSheet();
const { t } = useLingui();
const [acknowledgedReport, setAcknowledgedReport] = useState<string | null>(
null
);
@@ -73,15 +75,15 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
const report = useReportMatchScore(user?.id, {
onSuccess: (data: any) => {
if (data?.finalized) {
toast.success("Score confirmed 🍻");
toast.success(t`Score confirmed 🍻`);
if (data.downstreamReset) {
toast.error(
"Downstream matches were reset because the correction changed who advances."
t`Downstream matches were reset because the correction changed who advances.`
);
}
if (data.groupEditAfterKnockout) {
toast.error(
"Group result changed after the bracket was seeded — reseed the knockout stage if needed."
t`Group result changed after the bracket was seeded — reseed the knockout stage if needed.`
);
}
}
@@ -92,12 +94,12 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
onSuccess: (data: any) => {
if (data?.downstreamReset) {
toast.error(
"Downstream matches were reset because the correction changed who advances."
t`Downstream matches were reset because the correction changed who advances.`
);
}
if (data?.groupEditAfterKnockout) {
toast.error(
"Group result changed after the bracket was seeded — reseed the knockout stage if needed."
t`Group result changed after the bracket was seeded — reseed the knockout stage if needed.`
);
}
},
@@ -109,21 +111,21 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
(data: { home_cups: number; away_cups: number; ot_count: number }) => {
report.mutate({ data: { ...data, matchId: match.id } });
formSheet.close();
toast.success("Score submitted — waiting for the other team");
toast.success(t`Score submitted — waiting for the other team`);
},
[report, match.id, formSheet.close]
[report, match.id, formSheet.close, t]
);
const submitConfirm = useCallback(() => {
confirm.mutate({ data: { matchId: match.id } });
formSheet.close();
toast.success("Score confirmed 🍻");
}, [confirm, match.id, formSheet.close]);
toast.success(t`Score confirmed 🍻`);
}, [confirm, match.id, formSheet.close, t]);
const submitClear = useCallback(() => {
clear.mutate({ data: { matchId: match.id } });
toast.success("Report cleared");
}, [clear, match.id]);
toast.success(t`Report cleared`);
}, [clear, match.id, t]);
const openForm = useCallback(() => {
setAcknowledgedReport(pendingSignature);
@@ -143,7 +145,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
const formSheetEl = (
<Sheet
title={sheetTakenOver ? "Confirm Score" : "Report Score"}
title={sheetTakenOver ? t`Confirm Score` : t`Report Score`}
{...formSheet.props}
>
{sheetTakenOver ? (
@@ -152,16 +154,16 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
variant="light"
color="yellow"
icon={<LightningIcon size={18} weight="fill" />}
title={`${teamName(reportingTeam) ?? "The other team"} just reported a score`}
title={t`${teamName(reportingTeam) ?? t`The other team`} just reported a score`}
>
Does this look right?
<Trans>Does this look right?</Trans>
</Alert>
<Paper withBorder p="md">
<Group justify="center" gap="md" wrap="nowrap">
<Stack gap={2} align="center" style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={600} ta="center" lineClamp={1}>
{teamName(match.home) ?? "Home"}
{teamName(match.home) ?? t({ message: "Home", context: "match team" })}
</Text>
<Text fz={34} fw={700} lh={1.2}>
{match.reported_home_cups}
@@ -172,7 +174,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
</Text>
<Stack gap={2} align="center" style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={600} ta="center" lineClamp={1}>
{teamName(match.away) ?? "Away"}
{teamName(match.away) ?? t({ message: "Away", context: "match team" })}
</Text>
<Text fz={34} fw={700} lh={1.2}>
{match.reported_away_cups}
@@ -188,7 +190,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
onClick={submitConfirm}
loading={confirm.isPending}
>
Confirm {scoreText}
<Trans>Confirm {scoreText}</Trans>
</Button>
<Button
fullWidth
@@ -196,7 +198,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
onClick={dismissTakeover}
disabled={confirm.isPending}
>
Not right — enter our score
<Trans>Not right — enter our score</Trans>
</Button>
</Stack>
</Stack>
@@ -220,7 +222,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
onClick={openForm}
loading={report.isPending}
>
Report Score
<Trans>Report Score</Trans>
</Button>
{formSheetEl}
</>
@@ -237,11 +239,13 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
gap={compact ? 4 : "xs"}
>
<Text size="xs" c="dimmed" lineClamp={2} style={{ minWidth: 0 }}>
Waiting for {teamName(opposingTeam) ?? "the other team"} to confirm{" "}
·{" "}
<Text span fw={600} c="bright">
{scoreText}
</Text>
<Trans>
Waiting for {teamName(opposingTeam) ?? t`the other team`} to confirm{" "}
·{" "}
<Text span fw={600} c="bright">
{scoreText}
</Text>
</Trans>
</Text>
<Button
size={size}
@@ -251,7 +255,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
loading={clear.isPending}
style={{ flexShrink: 0 }}
>
Cancel
<Trans>Cancel</Trans>
</Button>
</Flex>
);
@@ -262,10 +266,12 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
<>
<Stack gap={6}>
<Text size="xs" c="dimmed" lineClamp={2}>
{teamName(reportingTeam) ?? "Opponent"} reported{" "}
<Text span fw={600} c="bright">
{scoreText}
</Text>
<Trans>
{teamName(reportingTeam) ?? t`Opponent`} reported{" "}
<Text span fw={600} c="bright">
{scoreText}
</Text>
</Trans>
</Text>
<Group gap="xs" wrap="nowrap">
<Button
@@ -275,7 +281,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
loading={confirm.isPending}
style={{ flex: 1 }}
>
Confirm
<Trans>Confirm</Trans>
</Button>
<Button
size={size}
@@ -283,7 +289,7 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
onClick={openForm}
disabled={confirm.isPending}
>
Not right
<Trans>Not right</Trans>
</Button>
</Group>
</Stack>
@@ -294,7 +300,9 @@ export const MatchReport: React.FC<MatchReportProps> = ({ match, compact }) => {
return (
<Text size="xs" c="dimmed" lineClamp={2}>
Pending: {scoreText} · reported by {teamName(reportingTeam) ?? "a team"}
<Trans>
Pending: {scoreText} · reported by {teamName(reportingTeam) ?? t`a team`}
</Trans>
</Text>
);
}
@@ -5,6 +5,7 @@ import { SeedBadge } from "./seed-badge";
import { TeamInfo } from "@/features/teams/types";
import AnimatedScore from "@/features/matches/components/animated-score";
import classes from "./match-slot.module.css";
import { Trans } from "@lingui/react/macro";
export type MatchSlotState = "winner" | "correct" | "incorrect";
@@ -97,11 +98,15 @@ export const MatchSlot: React.FC<MatchSlotProps> = ({
</Text>
) : from ? (
<Text c="dimmed" size="xs" truncate style={{ minWidth: 0, flex: 1 }}>
{from_loser ? "Loser" : "Winner"} of Match {from}
{from_loser ? (
<Trans>Loser of Match {from}</Trans>
) : (
<Trans>Winner of Match {from}</Trans>
)}
</Text>
) : (
<Text c="dimmed" size="xs" truncate style={{ minWidth: 0, flex: 1 }}>
TBD
<Trans>TBD</Trans>
</Text>
)}
</Flex>
@@ -1,4 +1,5 @@
import { Flex, Text, Select, Card } from "@mantine/core";
import { useLingui } from "@lingui/react/macro";
interface Team {
id: string;
@@ -11,9 +12,10 @@ interface SeedListProps {
}
export function SeedList({ teams, onSeedChange }: SeedListProps) {
const { t } = useLingui();
const seedOptions = teams.map((_, index) => ({
value: index.toString(),
label: `Seed ${index + 1}`,
label: t`Seed ${index + 1}`,
}));
return (
+19 -7
View File
@@ -1,9 +1,25 @@
import { msg } from "@lingui/core/macro";
import type { I18n } from "@lingui/core";
export interface GroupConfig {
num_groups: number;
advance_per_group: number;
}
const wildcardMsg = msg`Wildcard {n}`;
const groupRankMsg = msg`{group} {rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}`;
export const formatWildcardLabel = (i18n: I18n, n: number): string =>
i18n._({ ...wildcardMsg, values: { n } });
export const formatGroupRankLabel = (
i18n: I18n,
group: string,
rank: number
): string => i18n._({ ...groupRankMsg, values: { group, rank } });
export function getGroupLabel(
i18n: I18n,
seed: number | undefined,
groupConfig: GroupConfig | undefined
): string | undefined {
@@ -19,7 +35,7 @@ export function getGroupLabel(
if (seed > totalQualifiedTeams && wildcardsNeeded > 0) {
const wildcardNumber = seed - totalQualifiedTeams;
return `Wildcard ${wildcardNumber}`;
return formatWildcardLabel(i18n, wildcardNumber);
}
const pairIndex = Math.floor((seed - 1) / 2);
@@ -31,19 +47,15 @@ export function getGroupLabel(
const rank = rankIndex + 1;
const groupName = groupNames[groupIndex] || `${groupIndex + 1}`;
const rankSuffix =
rank === 1 ? "1st" : rank === 2 ? "2nd" : rank === 3 ? "3rd" : `${rank}th`;
return `${groupName} ${rankSuffix}`;
return formatGroupRankLabel(i18n, groupName, rank);
} else {
const groupIndex = (pairIndex + 1) % numGroups;
const rankIndex = advancePerGroup - 1 - Math.floor(pairIndex / numGroups);
const rank = rankIndex + 1;
const groupName = groupNames[groupIndex] || `${groupIndex + 1}`;
const rankSuffix =
rank === 1 ? "1st" : rank === 2 ? "2nd" : rank === 3 ? "3rd" : `${rank}th`;
return `${groupName} ${rankSuffix}`;
return formatGroupRankLabel(i18n, groupName, rank);
}
}