Merge branch 'main' into patch-misc-1

This commit is contained in:
Inkorest
2026-06-18 22:52:21 +08:00
74 changed files with 995 additions and 739 deletions

View File

@@ -11,6 +11,8 @@ interface LocaleTimeProps {
className?: string;
/** When `true`, renders inline; otherwise the element is displayed as a block. Defaults to block. */
inline?: boolean;
/** Optional test id forwarded to the rendered `<time>` element. */
"data-testid"?: string;
}
/**
@@ -25,6 +27,7 @@ export function LocaleTime({
options,
className,
inline,
"data-testid": testId,
}: LocaleTimeProps) {
const { formatter, isLoaded } = useDateTimeFormat(options);
@@ -33,9 +36,9 @@ export function LocaleTime({
return (
<time
data-testid={testId}
dateTime={dateObject.toISOString()}
className={clsx(
"reserve-one-lb",
{
block: !inline,
invisible: !isLoaded,

View File

@@ -13,6 +13,8 @@ interface LocaleTimeRangeProps {
className?: string;
/** When `true`, renders inline; otherwise the element is displayed as a block. Defaults to block. */
inline?: boolean;
/** Optional test id forwarded to the rendered element. */
"data-testid"?: string;
}
/**
@@ -29,6 +31,7 @@ export function LocaleTimeRange({
options,
className,
inline,
"data-testid": testId,
}: LocaleTimeRangeProps) {
const { formatter, isLoaded } = useDateTimeFormat(options);
@@ -38,8 +41,8 @@ export function LocaleTimeRange({
return (
<span
data-testid={testId}
className={clsx(
"reserve-one-lb",
{
block: !inline,
invisible: !isLoaded,

View File

@@ -17,7 +17,7 @@
font-size: var(--font-2xs);
}
& tbody tr:hover {
&:not(.noRowHover) tbody tr:hover {
background-color: var(--color-bg-high);
}

View File

@@ -1,9 +1,20 @@
import clsx from "clsx";
import styles from "./Table.module.css";
export function Table({ children }: { children: React.ReactNode }) {
export function Table({
children,
noRowHover,
}: {
children: React.ReactNode;
noRowHover?: boolean;
}) {
return (
<div className={styles.container}>
<table className={styles.table}>{children}</table>
<table
className={clsx(styles.table, { [styles.noRowHover]: noRowHover })}
>
{children}
</table>
</div>
);
}

View File

@@ -104,23 +104,22 @@
}
.memberGrid {
display: grid;
grid-template-areas:
"avatar name"
"tier meta";
grid-template-columns: auto 1fr;
column-gap: var(--s-2);
row-gap: var(--s-1);
align-items: center;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--s-1);
}
.memberLink {
grid-row: 1;
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
column-gap: var(--s-2);
display: flex;
align-items: center;
gap: var(--s-2);
}
.memberSecondRow {
display: flex;
align-items: center;
gap: var(--s-2);
}
.memberNameStack {
@@ -143,7 +142,6 @@
font: inherit;
text-align: inherit;
cursor: pointer;
width: 100%;
}
.friendCodeHeader {
@@ -168,12 +166,10 @@
}
.memberTier {
grid-area: tier;
justify-self: center;
}
.memberMetaArea {
grid-area: meta;
display: flex;
justify-content: center;
width: 24px;
flex-shrink: 0;
}
.memberMeta {

View File

@@ -164,10 +164,10 @@ function TeamRoster({
member={member}
className={styles.memberLink}
/>
<div className={styles.memberTier}>
<MemberTierPopover tier={member.tier} />
</div>
<div className={styles.memberMetaArea}>
<div className={styles.memberSecondRow}>
<div className={styles.memberTier}>
<MemberTierPopover tier={member.tier} />
</div>
<MemberMeta
plusTier={member.plusTier}
weaponPool={member.weaponPool}

View File

@@ -308,6 +308,7 @@ function ClockHeader({
className={clsx({
"text-lighter italic": isInThePast,
})}
data-testid="clock-header-time"
/>
) : (
<LocaleTime
@@ -316,6 +317,7 @@ function ClockHeader({
})}
date={date}
options={timeOptions}
data-testid="clock-header-time"
/>
)}
{hiddenEventsCount > 0 ? (

View File

@@ -23,6 +23,7 @@ import styles from "~/styles/front.module.css";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
BLANK_IMAGE_URL,
CALENDAR_PAGE,
LUTI_PAGE,
leaderboardsPage,
navIconUrl,
@@ -43,6 +44,7 @@ export default function FrontPage() {
<LeagueBanner />
<SeasonBanner />
<SplatoonRotations />
<TournamentShowcase />
<ResultHighlights />
<DiscoverFeatures />
<ChangelogList />
@@ -152,6 +154,27 @@ function LeagueBanner() {
);
}
function TournamentShowcase() {
const { t } = useTranslation(["front"]);
const data = useLoaderData<typeof loader>();
if (data.tournaments.showcase.length === 0) return null;
return (
<div className={styles.tournamentCards}>
<div className={clsx(styles.tournamentCardsSpacer, "scrollbar")}>
{data.tournaments.showcase.map((tournament) => (
<TournamentCard key={tournament.id} tournament={tournament} />
))}
</div>
<Link to={CALENDAR_PAGE} className={styles.tournamentCardsViewAllCard}>
<Image path={navIconUrl("medal")} size={36} alt="" />
{t("front:showcase.viewAll")}
</Link>
</div>
);
}
function ResultHighlights() {
const { t } = useTranslation(["front", "common"]);
const data = useLoaderData<typeof loader>();

View File

@@ -162,6 +162,19 @@
.currentValue {
font-variant-numeric: tabular-nums;
font-weight: var(--weight-semi);
}
.specialPointKits {
display: flex;
flex-direction: column;
gap: var(--s-0-5);
}
.specialPointKit {
display: flex;
align-items: center;
gap: var(--s-1);
}
.historyBadge {
@@ -194,18 +207,21 @@
.historyItem {
display: flex;
justify-content: space-between;
gap: var(--s-2);
align-items: baseline;
gap: var(--s-1-5);
}
.historyVersion {
color: var(--color-text-high);
font-size: var(--font-2xs);
font-style: italic;
white-space: nowrap;
}
.historyValue {
font-variant-numeric: tabular-nums;
color: var(--color-accent);
font-weight: var(--weight-semi);
color: var(--color-text-high);
min-width: 1.75rem;
}
.specialPointHistory {

View File

@@ -584,9 +584,6 @@ function SpecialPointCell({
kits: SpecialPointWithHistory[];
isExpanded: boolean;
}) {
const { t } = useTranslation(["analyzer"]);
const suffix = t("analyzer:suffix.specialPointsShort");
if (kits.length === 0) {
return (
<td className={styles.paramCell}>
@@ -597,13 +594,25 @@ function SpecialPointCell({
const kitsWithHistory = kits.filter((kit) => kit.history.length > 0);
const showHistory = isExpanded && kitsWithHistory.length > 0;
const multiKit = kits.length > 1;
return (
<td className={styles.paramCell}>
<div className={styles.cellContent}>
<span className={styles.currentValue}>
{kits.map((kit) => `${kit.current}${suffix}`).join(" / ")}
</span>
<div className={styles.specialPointKits}>
{kits.map((kit) => (
<div key={kit.weaponId} className={styles.specialPointKit}>
{multiKit ? (
<WeaponImage
weaponSplId={kit.weaponId}
variant="badge"
size={18}
/>
) : null}
<span className={styles.currentValue}>{kit.current}</span>
</div>
))}
</div>
{kitsWithHistory.length > 0 && !isExpanded ? (
<span className={styles.historyBadge}>{kitsWithHistory.length}</span>
) : null}
@@ -612,17 +621,17 @@ function SpecialPointCell({
<div className={styles.specialPointHistory}>
{kitsWithHistory.map((kit) => (
<div key={kit.weaponId} className={styles.specialPointHistoryKit}>
<WeaponImage
weaponSplId={kit.weaponId}
variant="badge"
size={24}
/>
{multiKit ? (
<WeaponImage
weaponSplId={kit.weaponId}
variant="badge"
size={16}
/>
) : null}
<div className={styles.specialPointHistoryKitList}>
{kit.history.toReversed().map(({ version, value }) => (
<div key={version} className={styles.historyItem}>
<span className={styles.historyValue}>
{`${value}${suffix}`}
</span>
<span className={styles.historyValue}>{value}</span>
<span className={styles.historyVersion}>{version}</span>
</div>
))}

View File

@@ -19,6 +19,7 @@ export function ModeMapPoolPicker({
onChange,
modeTabs,
onModeChange,
disabled,
}: {
mode: ModeShort;
amountToPick: number;
@@ -28,6 +29,8 @@ export function ModeMapPoolPicker({
/** When provided, the divider becomes a tab switcher between these modes. */
modeTabs?: ModeShort[];
onModeChange?: (mode: ModeShort) => void;
/** When true, stages can't be picked or removed (view-only). */
disabled?: boolean;
}) {
const [wigglingStageId, setWigglingStageId] = React.useState<StageId | null>(
null,
@@ -114,6 +117,7 @@ export function ModeMapPoolPicker({
const selected = stages.includes(stageId);
const onClick = () => {
if (disabled) return;
if (isTiebreaker) return;
if (banned) return;
if (selected) return handlePickedStageClick(stageId);
@@ -130,6 +134,7 @@ export function ModeMapPoolPicker({
banned={banned}
tiebreaker={isTiebreaker}
wiggle={wigglingStageId === stageId}
disabled={disabled}
testId={`map-pool-${mode}-${stageId}`}
/>
);
@@ -158,6 +163,7 @@ function MapButton({
banned,
tiebreaker,
wiggle,
disabled,
testId,
}: {
stageId: StageId;
@@ -166,6 +172,7 @@ function MapButton({
banned?: boolean;
tiebreaker?: boolean;
wiggle?: boolean;
disabled?: boolean;
testId: string;
}) {
const { t } = useTranslation(["game-misc"]);
@@ -181,7 +188,7 @@ function MapButton({
})}
style={{ "--map-image-url": `url("${stageImageUrl(stageId)}.avif")` }}
onClick={onClick}
disabled={banned}
disabled={disabled || banned}
type="button"
data-testid={testId}
/>

View File

@@ -63,10 +63,8 @@ const TOURNAMENT_SUB_QUOTA = 2;
export async function resolveSidebarData(userId: number | null) {
if (!userId) {
const tournamentsData =
await ShowcaseTournaments.categorizedTournamentsByUserId(null);
return {
events: showcaseEventsToSidebarEvents(tournamentsData.showcase),
events: [] as SidebarEvent[],
friends: [] as SidebarFriend[],
streams: await combinedStreamsCached(),
savedTournamentIds: [] as number[],
@@ -102,16 +100,9 @@ export async function resolveSidebarData(userId: number | null) {
const scrimEvents: SidebarEvent[] = scrimsData.map(scrimToSidebarEvent);
const personalEvents = [
...tournamentEvents,
...savedEvents,
...scrimEvents,
].sort((a, b) => a.startTime - b.startTime);
const events = (
personalEvents.length > 0
? personalEvents
: showcaseEventsToSidebarEvents(tournamentsData.showcase)
).slice(0, MAX_EVENTS_VISIBLE);
const events = [...tournamentEvents, ...savedEvents, ...scrimEvents]
.sort((a, b) => a.startTime - b.startTime)
.slice(0, MAX_EVENTS_VISIBLE);
const friends = resolveFriends(friendsWithActivity);
@@ -363,19 +354,6 @@ function resolveFriends(friendsWithActivity: FriendWithActivity[]) {
return result;
}
function showcaseEventsToSidebarEvents(
events: ShowcaseCalendarEvent[],
): SidebarEvent[] {
return events.map((e) => ({
id: e.id,
name: e.name,
url: e.url,
logoUrl: e.logoUrl,
startTime: e.startTime,
type: "tournament" as const,
}));
}
function rowToSidebarFriend(
row: FriendWithActivity,
subtitle: string,

View File

@@ -71,13 +71,15 @@ export default function TournamentAdminTeamsPage() {
>
Export
</SendouButton>
<LinkButton
size="small"
icon={<Plus />}
to={tournamentAdminRegistrationPage(tournament.ctx.id)}
>
Add new team
</LinkButton>
{!tournament.ctx.isFinalized ? (
<LinkButton
size="small"
icon={<Plus />}
to={tournamentAdminRegistrationPage(tournament.ctx.id)}
>
Add new team
</LinkButton>
) : null}
</div>
<Input
className={styles.searchInput}
@@ -98,7 +100,7 @@ export default function TournamentAdminTeamsPage() {
sort={sort}
onChange={setSort}
/>
<th>Actions</th>
{!tournament.ctx.isFinalized ? <th>Actions</th> : null}
<SortableTableHeader
label="Check-in"
sortKey="checkIn"
@@ -124,7 +126,10 @@ export default function TournamentAdminTeamsPage() {
))}
{sortedTeams.length === 0 ? (
<tr>
<td colSpan={maxRosterSize + 3} className={styles.noResults}>
<td
colSpan={maxRosterSize + (tournament.ctx.isFinalized ? 2 : 3)}
className={styles.noResults}
>
No registrations yet
</td>
</tr>
@@ -175,9 +180,11 @@ function TeamRow({
)}
</div>
</td>
<td>
<TeamRowMenu team={team} editPage={editPage} />
</td>
{!tournament.ctx.isFinalized ? (
<td>
<TeamRowMenu team={team} editPage={editPage} />
</td>
) : null}
<td>
<CheckInCell team={team} />
</td>

View File

@@ -3,11 +3,13 @@ import { useFetcher } from "react-router";
import { Divider } from "~/components/Divider";
import { FormMessage } from "~/components/FormMessage";
import { Input } from "~/components/Input";
import { Redirect } from "~/components/Redirect";
import { SubmitButton } from "~/components/SubmitButton";
import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-controls";
import { useUser } from "~/features/auth/core/user";
import { useTournament } from "~/features/tournament/routes/to.$id";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import { tournamentAdminPage } from "~/utils/urls";
import { BracketProgressionSelector } from "../../calendar/components/BracketProgressionSelector";
export { action } from "../actions/to.$id.admin.brackets.server";
@@ -26,6 +28,10 @@ export default function TournamentAdminBracketsPage() {
tournament.hasStarted &&
!tournament.ctx.isFinalized;
if (tournament.ctx.isFinalized && !showReopen) {
return <Redirect to={tournamentAdminPage(tournament.ctx.id)} />;
}
return (
<div className="stack lg">
{showEditBrackets ? (

View File

@@ -1,5 +1,7 @@
import { Redirect } from "~/components/Redirect";
import { useTournament } from "~/features/tournament/routes/to.$id";
import { SendouForm } from "~/form/SendouForm";
import { tournamentAdminPage } from "~/utils/urls";
import { adminStreamFormSchema } from "../tournament-admin-staff-schemas";
export { action } from "../actions/to.$id.admin.stream.server";
@@ -7,6 +9,10 @@ export { action } from "../actions/to.$id.admin.stream.server";
export default function TournamentAdminStreamPage() {
const tournament = useTournament();
if (tournament.ctx.isFinalized) {
return <Redirect to={tournamentAdminPage(tournament.ctx.id)} />;
}
return (
<SendouForm
schema={adminStreamFormSchema}

View File

@@ -54,14 +54,13 @@ export default function TournamentAdminLayout() {
tournament.hasStarted &&
!tournament.ctx.isFinalized;
const showStaffTab = tournament.isAdmin(user);
const showBracketsTab =
!tournament.isLeagueSignup || showEditBrackets || showReopen;
const showBracketsTab = tournament.ctx.isFinalized
? showReopen
: !tournament.isLeagueSignup || showEditBrackets;
const showStreamTab = !tournament.ctx.isFinalized;
const showSeedsTab = !tournament.hasStarted && !tournament.isLeagueSignup;
if (
!tournament.isOrganizer(user) ||
(tournament.ctx.isFinalized && !DANGEROUS_CAN_ACCESS_DEV_CONTROLS)
) {
if (!tournament.isOrganizer(user)) {
return <Redirect to={tournamentPage(tournament.ctx.id)} />;
}
@@ -132,9 +131,11 @@ export default function TournamentAdminLayout() {
{t("tournament:admin.tab.staff")}
</SendouTab>
) : null}
<SendouTab id="stream" href={`${adminPage}/stream`} icon={<Tv />}>
{t("tournament:admin.tab.stream")}
</SendouTab>
{showStreamTab ? (
<SendouTab id="stream" href={`${adminPage}/stream`} icon={<Tv />}>
{t("tournament:admin.tab.stream")}
</SendouTab>
) : null}
{showBracketsTab ? (
<SendouTab
id="brackets"

View File

@@ -25,7 +25,6 @@ import { useAutoRerender } from "~/hooks/useAutoRerender";
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { useHydrated } from "~/hooks/useHydrated";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import invariant from "~/utils/invariant";
import {
LOG_IN_URL,
SENDOU_INK_BASE_URL,
@@ -69,9 +68,12 @@ export default function TournamentRegisterPage() {
return (
<div className={clsx("stack lg", containerClassName("normal"))}>
{isRegularMemberOfATeam ? (
<div className="stack md items-center">
<Alert>{t("tournament:pre.inATeam")}</Alert>
<LeaveTeamControl />
<div className="stack md">
<Alert>{t("tournament:pre.captainOnlyEdit")}</Alert>
<div className="stack md items-center">
<LeaveTeamControl />
</div>
<RegistrationForms readOnly />
</div>
) : registrationClosedForNonParticipant ? (
<Alert>{t("tournament:pre.registrationClosed")}</Alert>
@@ -148,11 +150,15 @@ function PleaseLogIn() {
);
}
function RegistrationForms() {
function RegistrationForms({ readOnly = false }: { readOnly?: boolean }) {
const data = useLoaderData<TournamentRegisterPageLoader>();
const user = useUser();
const tournament = useTournament();
if (readOnly) {
return <ReadOnlyRegistrationForms />;
}
const ownTeam = tournament.ownedTeamByUser(user);
const ownTeamCheckedIn = Boolean(ownTeam && ownTeam.checkIns.length > 0);
const hasFriendCodeSet = Boolean(user?.friendCode);
@@ -212,6 +218,32 @@ function RegistrationForms() {
);
}
function ReadOnlyRegistrationForms() {
const user = useUser();
const tournament = useTournament();
const team = tournament.teamMemberOfByUser(user);
if (!team) return null;
const checkedIn = team.checkIns.length > 0;
return (
<div className="stack lg">
<RegistrationProgress
checkedIn={checkedIn}
name={team.name}
mapPool={team.mapPool ?? undefined}
members={team.members}
/>
<TeamInfo ownTeam={team} canUnregister={false} readOnly />
<FillRoster ownTeam={team} ownTeamCheckedIn={checkedIn} readOnly />
{tournament.teamsPrePickMaps ? (
<CounterPickMapPoolPicker readOnly mapPool={team.mapPool ?? []} />
) : null}
</div>
);
}
function RegistrationProgress({
checkedIn,
name,
@@ -419,16 +451,22 @@ function CheckIn({
function TeamInfo({
ownTeam,
canUnregister,
readOnly = false,
}: {
ownTeam?: TournamentDataTeam | null;
canUnregister: boolean;
readOnly?: boolean;
}) {
const { t } = useTranslation(["tournament", "common"]);
const tournament = useTournament();
const defaultValues: Partial<RegisterTeamFormValues> = {
teamId: ownTeam?.team ? String(ownTeam.team.id) : null,
pickUpName: ownTeam?.team ? null : (ownTeam?.name ?? ""),
teamId: readOnly ? null : ownTeam?.team ? String(ownTeam.team.id) : null,
pickUpName: readOnly
? (ownTeam?.name ?? "")
: ownTeam?.team
? null
: (ownTeam?.name ?? ""),
logo:
!ownTeam?.team &&
ownTeam?.pickupAvatarUrl &&
@@ -488,19 +526,34 @@ function TeamInfo({
className="stack md items-center"
submitButtonText={t("common:actions.save")}
submitButtonTestId="save-team-button"
readOnly={readOnly}
>
<RegisterTeamFields />
<RegisterTeamFields readOnly={readOnly} />
</SendouForm>
</section>
</div>
);
}
function RegisterTeamFields() {
function RegisterTeamFields({ readOnly = false }: { readOnly?: boolean }) {
const data = useLoaderData<TournamentRegisterPageLoader>();
const tournament = useTournament();
const { values } = useFormFieldContext();
if (readOnly) {
return (
<>
<div className={styles.sectionInputContainer}>
<FormField name="pickUpName" />
</div>
<div className={styles.sectionInputContainer}>
<FormField name="logo" />
</div>
<FormField name="prefersNotToHost" />
</>
);
}
const isLinked = Boolean(values.teamId);
const teamOptions = (data?.teams ?? []).map((team) => ({
@@ -583,12 +636,13 @@ function GoogleFormsLink() {
function FillRoster({
ownTeam,
ownTeamCheckedIn,
readOnly = false,
}: {
ownTeam: TournamentDataTeam;
ownTeamCheckedIn: boolean;
readOnly?: boolean;
}) {
const data = useLoaderData<TournamentRegisterPageLoader>();
const user = useUser();
const tournament = useTournament();
const { copyToClipboard, copySuccess } = useCopyToClipboard();
const { t } = useTranslation(["common", "tournament"]);
@@ -598,8 +652,7 @@ function FillRoster({
inviteCode: ownTeam.inviteCode!,
})}`;
const { members: ownTeamMembers } = tournament.ownedTeamByUser(user) ?? {};
invariant(ownTeamMembers, "own team members should exist");
const ownTeamMembers = ownTeam.members;
const missingMembers = Math.max(
tournament.minMembersPerTeam - ownTeamMembers.length,
@@ -612,11 +665,14 @@ function FillRoster({
);
const showDeleteMemberSection =
(!ownTeamCheckedIn && ownTeamMembers.length > 1) ||
(ownTeamCheckedIn && ownTeamMembers.length > tournament.minMembersPerTeam);
!readOnly &&
((!ownTeamCheckedIn && ownTeamMembers.length > 1) ||
(ownTeamCheckedIn &&
ownTeamMembers.length > tournament.minMembersPerTeam));
const playersAvailableToDirectlyAdd = (() => {
return (data!.friendPlayers?.friends ?? []).filter((user) => {
if (readOnly) return [];
return (data?.friendPlayers?.friends ?? []).filter((user) => {
const isNotInTeam = tournament.ctx.teams.every((team) =>
team.members.every((member) => member.userId !== user.id),
);
@@ -629,7 +685,7 @@ function FillRoster({
})();
const teamIsFull = ownTeamMembers.length >= tournament.maxMembersPerTeam;
const canAddMembers = !teamIsFull && tournament.registrationOpen;
const canAddMembers = !teamIsFull && tournament.registrationOpen && !readOnly;
return (
<div>
@@ -842,13 +898,19 @@ function DeleteMember({ members }: { members: TournamentDataTeam["members"] }) {
);
}
function CounterPickMapPoolPicker() {
function CounterPickMapPoolPicker({
readOnly = false,
mapPool,
}: {
readOnly?: boolean;
mapPool?: NonNullable<TournamentDataTeam["mapPool"]>;
}) {
const { t } = useTranslation(["common", "game-misc", "tournament"]);
const tournament = useTournament();
const fetcher = useFetcher();
const data = useLoaderData<TournamentRegisterPageLoader>();
const [counterPickMaps, setCounterPickMaps] = React.useState(
data?.mapPool ?? [],
mapPool ?? data?.mapPool ?? [],
);
const counterPickMapPool = new MapPool(counterPickMaps);
@@ -899,14 +961,15 @@ function CounterPickMapPoolPicker() {
...stageIds.map((stageId) => ({ mode, stageId })),
])
}
disabled={readOnly}
/>
);
})}
{validateCounterPickMapPool(
counterPickMapPool,
isOneModeTournamentOf,
tournament.ctx.tieBreakerMapPool,
) === "VALID" ? (
{readOnly ? null : validateCounterPickMapPool(
counterPickMapPool,
isOneModeTournamentOf,
tournament.ctx.tieBreakerMapPool,
) === "VALID" ? (
<SubmitButton
_action="UPDATE_MAP_POOL"
state={fetcher.state}

View File

@@ -102,7 +102,7 @@ function ResultsTable({ standings }: { standings: Standing[] }) {
let rowDarkerBg = false;
return (
<Table>
<Table noRowHover>
<thead>
<tr>
<th>Standing</th>
@@ -151,7 +151,7 @@ function ResultsTable({ standings }: { standings: Standing[] }) {
return (
<tr
key={standing.team.id}
className={rowDarkerBg ? "bg-darker-transparent" : undefined}
className={rowDarkerBg ? styles.standingsRowAlt : undefined}
>
<td className="text-md">
{typeof placement === "number" ? (

View File

@@ -4,7 +4,6 @@ import {
Outlet,
type ShouldRevalidateFunction,
useLoaderData,
useMatches,
useOutletContext,
} from "react-router";
import { containerClassName, Main } from "~/components/Main";
@@ -98,7 +97,6 @@ export function TournamentLayout() {
[data],
);
const [bracketExpanded, setBracketExpanded] = React.useState(true);
const mainBreakout = useActiveRouteMainBreakout();
useTournamentChatLabels(tournament);
@@ -134,25 +132,15 @@ export function TournamentLayout() {
</>
);
// Always render within the breakout container so the nav (and content) keep a
// consistent width across routes, avoiding a layout shift when switching tabs.
return (
<Main bigger breakoutContainer={mainBreakout}>
{mainBreakout ? (
<div className={containerClassName("wide")}>{content}</div>
) : (
content
)}
<Main breakoutContainer>
<div className={containerClassName("wide")}>{content}</div>
</Main>
);
}
function useActiveRouteMainBreakout(): boolean {
const matches = useMatches();
return matches.some(
(match) => (match.handle as SendouRouteHandle | undefined)?.mainBreakout,
);
}
type TournamentContext = {
tournament: Tournament;
bracketExpanded: boolean;

View File

@@ -504,6 +504,10 @@
border-color: var(--color-error);
}
.standingsRowAlt > td {
background-color: var(--color-bg-high);
}
.standingsTeamName {
min-width: 125px;
display: flex;

View File

@@ -6,20 +6,22 @@
margin-block: var(--s-2);
}
@media (min-width: 1425px) {
.embedContainer {
position: fixed;
left: max(24px, calc((100vw - 800px) / 2 - 320px - 48px));
top: 120px;
width: 320px;
margin-block: 0;
}
}
/* When the form's left margin can fit the embed (measured in JS so it accounts
for the side nav being collapsed and the chat sidebar being open), float it
just left of the form. The rail is anchored to the form's own left edge, so
the side nav offset cancels out and it never overlaps the nav. Its width is
set inline by the same JS. The rail spans the full form height while the
embed sticks within it, so it stays in view while scrolling the long form. */
.embedRail.floating {
position: absolute;
top: 0;
bottom: 0;
right: calc(100% + var(--s-6));
@media (min-width: 1700px) {
.embedContainer {
left: max(24px, calc((100vw - 800px) / 2 - 400px - 48px));
width: 400px;
& .embedContainer {
position: sticky;
top: calc(var(--layout-nav-height) + var(--s-4));
margin-block: 0;
}
}

View File

@@ -1,3 +1,4 @@
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLoaderData } from "react-router";
@@ -13,6 +14,7 @@ import { FormFieldWrapper } from "~/form/fields/FormFieldWrapper";
import type { WeaponPoolItem } from "~/form/fields/WeaponPoolFormField";
import type { FormRenderProps } from "~/form/SendouForm";
import { SendouForm, useFormFieldContext } from "~/form/SendouForm";
import { useIsomorphicLayoutEffect } from "~/hooks/useIsomorphicLayoutEffect";
import { useRecentlyReportedWeapons } from "~/hooks/useRecentlyReportedWeapons";
import type { MainWeaponId, StageId } from "~/modules/in-game-lists/types";
import { useHasRole } from "~/modules/permissions/hooks";
@@ -127,6 +129,7 @@ function YouTubeEmbedWrapper({
onPlayerReady: (player: YT.Player) => void;
}) {
const { values } = useFormFieldContext();
const floatWidth = useFloatingEmbedWidth();
const youtubeUrl = values.youtubeUrl as string | undefined;
if (!youtubeUrl) return null;
@@ -135,12 +138,55 @@ function YouTubeEmbedWrapper({
if (!videoId) return null;
return (
<div className={styles.embedContainer}>
<YouTubeEmbed id={videoId} enableApi onPlayerReady={onPlayerReady} />
<div
className={clsx(styles.embedRail, { [styles.floating]: floatWidth })}
style={floatWidth ? { width: floatWidth } : undefined}
>
<div className={styles.embedContainer}>
<YouTubeEmbed id={videoId} enableApi onPlayerReady={onPlayerReady} />
</div>
</div>
);
}
const EMBED_RAIL_GAP = 24; // mirrors var(--s-6)
const EMBED_FLOAT_WIDTHS = [400, 320] as const;
/**
* Returns the width to float the embed at when the form's left margin can fit
* it (widest that fits), or `null` to leave it in flow above the fields.
* Measures the form's actual left margin so it accounts for the side nav being
* collapsed and the chat sidebar being open, neither of which a media query can
* see.
*/
function useFloatingEmbedWidth(): number | null {
const [leftMargin, setLeftMargin] = useState(0);
useIsomorphicLayoutEffect(() => {
const main = document.querySelector("main");
const container = main?.parentElement;
if (!main || !container) return;
const measure = () => {
setLeftMargin(
main.getBoundingClientRect().left -
container.getBoundingClientRect().left,
);
};
measure();
const observer = new ResizeObserver(measure);
observer.observe(container);
return () => observer.disconnect();
}, []);
return (
EMBED_FLOAT_WIDTHS.find((width) => leftMargin >= width + EMBED_RAIL_GAP) ??
null
);
}
type VodFormFieldComponent = FormRenderProps<
typeof vodFormBaseSchema.shape
>["FormField"];

View File

@@ -77,6 +77,7 @@ export function FormField({
canRemoveItem,
}: FormFieldProps) {
const context = useOptionalFormFieldContext();
const isDisabled = disabled ?? context?.readOnly ?? false;
const fieldSchema = React.useMemo(() => {
if (field) return field;
@@ -183,7 +184,7 @@ export function FormField({
<InputFormField
{...commonProps}
{...formField}
disabled={disabled}
disabled={isDisabled}
value={value as string}
onChange={handleChange as (v: string) => void}
/>
@@ -195,7 +196,7 @@ export function FormField({
<InGameNameFormField
{...commonProps}
{...formField}
disabled={disabled}
disabled={isDisabled}
value={value as string}
onChange={handleChange as (v: string) => void}
/>
@@ -207,7 +208,7 @@ export function FormField({
<SwitchFormField
{...commonProps}
{...formField}
isDisabled={disabled}
isDisabled={isDisabled}
checked={value as boolean}
onChange={handleChange as (v: boolean) => void}
/>
@@ -219,7 +220,7 @@ export function FormField({
<TextareaFormField
{...commonProps}
{...formField}
disabled={disabled}
disabled={isDisabled}
value={value as string}
onChange={handleChange as (v: string) => void}
/>
@@ -346,7 +347,7 @@ export function FormField({
<ImageFormField
{...commonProps}
{...formField}
disabled={disabled}
disabled={isDisabled}
value={value as ImageFieldValue}
onChange={handleChange as (v: ImageFieldValue) => void}
/>

View File

@@ -37,6 +37,7 @@ export interface FormContextValue<T extends z.ZodRawShape = z.ZodRawShape> {
clearServerError: (name: string) => void;
onFieldChange?: (name: string, newValue: unknown) => void;
hideRequiredIndicator: boolean;
readOnly: boolean;
values: Record<string, unknown>;
setValue: (name: string, value: unknown) => void;
setValueFromPrev: (name: string, updater: (prev: unknown) => unknown) => void;
@@ -104,6 +105,11 @@ type BaseFormProps<T extends z.ZodRawShape> = {
* adds noise (e.g. the settings page).
*/
hideRequiredIndicator?: boolean;
/**
* When true, renders the form for viewing only: every field is disabled and
* the submit button is hidden.
*/
readOnly?: boolean;
onApply?: (values: z.infer<z.ZodObject<T>>) => void;
secondarySubmit?: React.ReactNode;
/**
@@ -150,6 +156,7 @@ export function SendouForm<T extends z.ZodRawShape>({
className,
fullWidth,
hideRequiredIndicator = false,
readOnly = false,
onApply,
secondarySubmit,
onSuccess,
@@ -266,6 +273,7 @@ export function SendouForm<T extends z.ZodRawShape>({
onFieldChange:
autoSubmit || autoApply ? actions.onFieldChange : undefined,
hideRequiredIndicator,
readOnly,
setValue: actions.setValue,
setValueFromPrev: actions.setValueFromPrev,
revalidateAll: actions.revalidateAll,
@@ -281,6 +289,7 @@ export function SendouForm<T extends z.ZodRawShape>({
autoSubmit,
autoApply,
hideRequiredIndicator,
readOnly,
fetcher.state,
store,
actions,
@@ -303,7 +312,7 @@ export function SendouForm<T extends z.ZodRawShape>({
<>
{title ? <h2 className={styles.title}>{title}</h2> : null}
<React.Fragment key={locationKey}>{resolvedChildren}</React.Fragment>
{autoSubmit || autoApply ? null : (
{autoSubmit || autoApply || readOnly ? null : (
<div className="mt-4 stack horizontal md mx-auto justify-center items-center">
<SubmitButton
_action={_action}
@@ -673,6 +682,7 @@ export function useFormFieldContext(): FormContextValue {
clearServerError: context.clearServerError,
onFieldChange: context.onFieldChange,
hideRequiredIndicator: context.hideRequiredIndicator,
readOnly: context.readOnly,
values,
setValue: context.setValue,
setValueFromPrev: context.setValueFromPrev,

View File

@@ -1,17 +1,25 @@
import { databaseTimestampToDate } from "~/utils/dates";
import { useUserIntlPreference } from "./useUserIntlPreference";
/**
* Zero-width space rendered (hidden via `invisible`) during SSR so the element
* reserves exactly one normal text line, avoiding layout shift on hydration
* without an empty box's baseline quirks.
*/
const SSR_PLACEHOLDER = "\u200b";
const SSR_FORMATTER = {
format: (_date: Date | number) => null,
formatRange: (_from: Date | number, _to: Date | number) => null,
format: (_date: Date | number) => SSR_PLACEHOLDER,
formatRange: (_from: Date | number, _to: Date | number) => SSR_PLACEHOLDER,
};
/**
* SSR-safe wrapper around `Intl.DateTimeFormat`.
*
* Uses the user's locale and hour cycle preferences via `useUserIntlPreference`.
* Before hydration the returned formatter's methods return `null` so that
* server output matches the initial client render.
* Before hydration the returned formatter's methods return a zero-width space
* placeholder so that server output matches the initial client render while
* reserving one text line.
*
* Inputs accept either a `Date` or a database timestamp (`number`); numbers
* are converted via `databaseTimestampToDate`.

View File

@@ -3,7 +3,7 @@
--card-height: 93px;
display: flex;
flex-direction: column;
gap: var(--s-8);
gap: var(--s-10);
}
.changeLogImg {
@@ -125,6 +125,12 @@
}
}
.tournamentCards {
display: flex;
justify-content: space-between;
gap: var(--s-2);
}
.tournamentCardsSpacer {
display: flex;
gap: var(--s-2);
@@ -136,6 +142,26 @@
overflow-x: auto;
}
.tournamentCardsViewAllCard {
background-color: var(--color-bg-higher);
height: var(--card-height);
border-radius: var(--radius-box);
display: grid;
place-items: center;
padding: var(--s-2);
font-size: var(--font-2xs);
font-weight: var(--weight-bold);
text-align: center;
color: var(--color-text);
transition: 0.2s ease-out;
min-width: 90px;
max-width: 90px;
&:hover {
background-color: var(--color-bg-high);
}
}
.resultHighlights {
display: flex;
gap: var(--s-2);

View File

@@ -388,10 +388,6 @@
visibility: hidden;
}
.reserve-one-lb {
min-height: 1lh;
}
.whitespace-pre-wrap {
white-space: pre-wrap;
}

View File

@@ -127,9 +127,7 @@ test.describe("Calendar", () => {
await navigate({ page: gb.page, url: calendarPage() });
const firstClockText = (page: Page) =>
page
.locator("[class*='clockHeader'] [class*='reserve-one-lb']")
.first();
page.getByTestId("clock-header-time").first();
const caTime = await firstClockText(ca.page).textContent();
const gbTime = await firstClockText(gb.page).textContent();

View File

@@ -108,7 +108,10 @@ test.describe("Navigation", () => {
await expect(
mobileMenu.getByRole("link", { name: "SendouQ" }),
).not.toBeVisible();
const friendsViewAll = page.getByRole("link", { name: /View all/ });
const friendsViewAll = page.getByRole("link", {
name: "View all",
exact: true,
});
await expect(friendsViewAll).toBeVisible();
// Switch to You panel via ghost tab (nth(4) = "you")
@@ -124,7 +127,10 @@ test.describe("Navigation", () => {
.locator("[class*='ghostTab']:not([class*='ghostTabBar'])")
.nth(2)
.dispatchEvent("click");
const tourneysViewAll = page.getByRole("link", { name: /View all/ });
const tourneysViewAll = page.getByRole("link", {
name: "View all",
exact: true,
});
await expect(tourneysViewAll).toBeVisible();
// Close panel via X button

View File

@@ -15,7 +15,17 @@ test.describe("Weapon parameters", () => {
await selectWeapon({ page, name: "Splattershot" });
await page.getByRole("link", { name: /Raw parameters/ }).click();
// Selecting the weapon updates the URL search params asynchronously, which in
// turn updates the "Raw parameters" link's href. Wait for the href to reflect the
// selection before clicking, otherwise the click can navigate to the stale default.
const rawParametersLink = page.getByRole("link", {
name: /Raw parameters/,
});
await expect(rawParametersLink).toHaveAttribute(
"href",
/\/params\/splattershot/,
);
await rawParametersLink.click();
await expect(page).toHaveURL(/\/params\/splattershot/);
// Filtering: hide a weapon column. Wait for a sibling column to render after the client-side

View File

@@ -67,9 +67,7 @@ test.describe("Settings", () => {
url: CALENDAR_PAGE,
});
const clockTime = page
.locator("[class*='clockHeader'] [class*='reserve-one-lb']")
.first();
const clockTime = page.getByTestId("clock-header-time").first();
const initialTime = await clockTime.textContent();
expect(initialTime).toMatch(/AM|PM/);

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "Tilmeldingen til turneringen kan frit ændres inden turneringen starter",
"pre.logIn": "Log in for at tilmelde dig",
"pre.inATeam": "Du er allerede en del af et hold til denne begivenhed",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "Tjek-ind kan gøres imellem {{start}} og {{finish}}",
"pre.checkIn.over": "Tjek-ind er forbi",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "Registrierung kann vor Turnierstart frei verändert werden",
"pre.logIn": "Logge dich zum Registrieren ein",
"pre.inATeam": "Du bist in einem Team für dieses Event",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "Check-in ist zwischen {{start}} und {{finish}} geöffnet",
"pre.checkIn.over": "Check-in ist vorbei",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "Unranked",
"showcase.card.winner": "Winner",
"showcase.results": "Recent results",
"showcase.viewAll": "View all tournaments",
"leaderboards.topPlayers": "Top players",
"leaderboards.topTeams": "Top teams",
"leaderboards.viewFull": "View full leaderboard",

View File

@@ -31,6 +31,7 @@
"pre.footer": "Registration can be freely changed before the tournament starts",
"pre.logIn": "Log in to register",
"pre.inATeam": "You are in a team for this event",
"pre.captainOnlyEdit": "You are in a team for this event. Only the team captain can edit the registration.",
"pre.registrationClosed": "Registration for this tournament has closed",
"pre.checkIn.range": "Check-in is open between {{start}} and {{finish}}",
"pre.checkIn.over": "Check-in is over",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "Sin rango",
"showcase.card.winner": "Ganador",
"showcase.results": "Resultados recientes",
"showcase.viewAll": "Ver todos los torneos",
"leaderboards.topPlayers": "Mejores jugadores",
"leaderboards.topTeams": "Mejores equipos",
"leaderboards.viewFull": "Ver clasificación completa",

View File

@@ -31,6 +31,7 @@
"pre.footer": "Registro puede ser ajustado antes de que empieze el torneo",
"pre.logIn": "Ingresa al sitio para registrar",
"pre.inATeam": "Estás en un equipo para este evento",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "El check-in está abierto entre {{start}} y {{finish}}",
"pre.checkIn.over": "Se acabó check-in",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "Registro puede ser ajustado antes de que empieze el torneo",
"pre.logIn": "Ingresa al sitio para registrar",
"pre.inATeam": "Estás en un equipo para este evento",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "El check-in está abierto entre {{start}} y {{finish}}",
"pre.checkIn.over": "Se acabó check-in",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "L'inscription peut être librement modifiée avant le début du tournoi",
"pre.logIn": "Connectez-vous pour vous inscrire",
"pre.inATeam": "Vous faites partie d'une équipe pour cet événement",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "L'enregistrement est ouvert entre {{start}} et {{finish}}",
"pre.checkIn.over": "L'enregistrement est fini",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "Non classé",
"showcase.card.winner": "Gagnant",
"showcase.results": "Résultats récents",
"showcase.viewAll": "Regarder tous les tournois",
"leaderboards.topPlayers": "Top Joueurs",
"leaderboards.topTeams": "Top Teams",
"leaderboards.viewFull": "Voir tout le Leaderboard",

View File

@@ -31,6 +31,7 @@
"pre.footer": "L'inscription peut être librement modifiée avant le début du tournoi",
"pre.logIn": "Connectez-vous pour vous inscrire",
"pre.inATeam": "Vous faites partie d'une équipe pour cet événement",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "L'enregistrement est ouvert entre {{start}} et {{finish}}",
"pre.checkIn.over": "L'enregistrement est fini",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "הרשמה ניתנת לשינוי עד שהטורניר מתחיל",
"pre.logIn": "התחבר כדי להירשם",
"pre.inATeam": "הנך בצוות לאירוע",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "צ'ק-אין פתוח בין {{start}} ל-{{finish}}",
"pre.checkIn.over": "צ'ק-אין נגמר",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "Non classificata",
"showcase.card.winner": "Vincitore",
"showcase.results": "Risultati recenti",
"showcase.viewAll": "Visualizza tutti i tornei",
"leaderboards.topPlayers": "Top giocatori",
"leaderboards.topTeams": "Top squadre",
"leaderboards.viewFull": "Visualizza la classifica completa",

View File

@@ -31,6 +31,7 @@
"pre.footer": "L'iscrizione può essere cambiata liberamente prima del torneo",
"pre.logIn": "Accedi per iscriverti",
"pre.inATeam": "Sei in un team per questo evento",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "Il Check-in è aperto tra {{start}} e {{finish}}",
"pre.checkIn.over": "Il Check-in è chiuso",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "参加登録はトーナメント開始前であればいつでも変更できます",
"pre.logIn": "ログインして登録する",
"pre.inATeam": "あなたはこのイベントでチームに参加しています",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "チェックインは {{start}} から {{finish}} までのあいだ受け付けています",
"pre.checkIn.over": "チェックインは終了しました",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "",
"pre.logIn": "",
"pre.inATeam": "",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "",
"pre.checkIn.over": "",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "",
"pre.logIn": "",
"pre.inATeam": "",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "",
"pre.checkIn.over": "",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "",
"pre.logIn": "",
"pre.inATeam": "",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "",
"pre.checkIn.over": "",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "",
"showcase.card.winner": "",
"showcase.results": "",
"showcase.viewAll": "",
"leaderboards.topPlayers": "",
"leaderboards.topTeams": "",
"leaderboards.viewFull": "",

View File

@@ -31,6 +31,7 @@
"pre.footer": "O registro pode ser livremente alterado antes do torneio começar",
"pre.logIn": "Faça o login para registrar",
"pre.inATeam": "Você está em um time para este evento",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "O check-in está aberto entre {{start}} e {{finish}}",
"pre.checkIn.over": "O check-in acabou",

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "Без Рейтинга",
"showcase.card.winner": "Победитель",
"showcase.results": "Недавние результаты",
"showcase.viewAll": "Посмотреть все турниры",
"leaderboards.topPlayers": "Топ игроков",
"leaderboards.topTeams": "Топ команд",
"leaderboards.viewFull": "Посмотреть таблицы лидеров",

View File

@@ -31,6 +31,7 @@
"pre.footer": "До начала турнира вся информация о команде может быть изменена",
"pre.logIn": "Войдите, чтобы зарегистрироваться",
"pre.inATeam": "Вы уже состоите в команде, записанной на этот турнир",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "Чек-ин открыт с {{start}} по {{finish}}",
"pre.checkIn.over": "Чек-ин закрыт",

View File

@@ -152,7 +152,7 @@
"patch": "游戏版本:",
"abilityPoints.short": "AP",
"abilityChunks": "所需碎片",
"torpedoExplanation": "根据游戏机制,你无法同时使用个鱼雷。",
"torpedoExplanation": "同时只能使用个鱼雷。",
"trackingSubDefExplanation": "定点侦测器、墨汁陷阱和标线器的索敌时间是根据没有携带提升次要武器性能的敌人计算得出。",
"damageSubDefExplanation": "洒墨器和斯普拉防护墙未在表内显示,但是它们所造成的伤害也会被减轻次要武器影响的效果降低。",
"distanceInline": "距离: {{value}}",

View File

@@ -41,13 +41,13 @@
"tag.desc.LOW": "本次赛事限制选手水平。",
"tag.desc.COUNT": "本次赛事限制参赛队伍数量。",
"tag.desc.LAN": "本次赛事将于线下举行。",
"tag.desc.QUALIFIER": "本次赛事其他赛事的资格赛。",
"tag.desc.QUALIFIER": "本次赛事其他赛事的资格赛。",
"tag.desc.SZ": "本次赛事仅限真格区域。",
"tag.desc.TW": "本次赛事包括占地对战。",
"tag.desc.S1": "本次赛事为斯普拉遁 1 的赛事。",
"tag.desc.S2": "本次赛事为斯普拉遁 2 的赛事。",
"tag.desc.SR": "鲑鱼跑赛事。",
"tag.desc.CARDS": "占地斗士赛事。",
"tag.desc.SR": "本次赛事为鲑鱼跑赛事。",
"tag.desc.CARDS": "本次赛事为占地斗士赛事。",
"icalFeed": "iCal",
"filter.button": "筛选",
"filter.heading": "筛选赛事日程",

View File

@@ -201,10 +201,10 @@
"tag.name.SPECIAL": "特殊规则",
"tag.name.ART": "插画奖励",
"tag.name.MONEY": "奖金",
"tag.name.REGION": "地区限制",
"tag.name.LOW": "水平上限",
"tag.name.HIGH": "水平下限",
"tag.name.COUNT": "队伍数量限制",
"tag.name.REGION": "限制地区",
"tag.name.LOW": "水平上限",
"tag.name.HIGH": "水平下限",
"tag.name.COUNT": "限制队伍数量",
"tag.name.LAN": "线下",
"tag.name.QUALIFIER": "资格赛",
"tag.name.ONES": "1v1",
@@ -330,9 +330,9 @@
"fc.altingWarning": "您必须始终使用与此好友代码绑定的账号进行游戏。使用其他账号将被视为开小号,这违反了相关规则。",
"fc.changeHelp": "如果您想修改好友编号,请在本网站的 Discord 服务器的 helpdesk 频道联系工作人员。",
"settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.label": "配装:禁用技能自动排序",
"settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "在您的个人主页之外,配装技能会自动排序,以便相同的技能排列在一起。开启此设置后,您在所有页面看到的技能都将保持其最初创建时的原始顺序。",
"settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "对抗战:禁止非好友将我加入临时队",
"settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "默认情况下任何人都可以添加您。如果您希望只允许好友将您加入临时队,请启用此设置。",
"settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "在您的个人主页之外,配装技能会自动排序,以便使相同的技能排列在一起。开启此设置后,您在所有页面看到的技能都将保持其最初创建时的原始顺序。",
"settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "对抗战:禁止非好友将我加入临时队",
"settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "默认情况下任何人都可以添加您。如果您希望只允许好友将您加入临时队,请启用此设置。",
"settings.UPDATE_NO_SCREEN.label": "【无障碍】避开含有“浮墨幕墙”的比赛",
"settings.UPDATE_NO_SCREEN.bottomText": "此设置将应用于赛事、对抗战和 SendouQ。",
"settings.notifications.title": "推送通知",

View File

@@ -1,303 +1,303 @@
{
"submit": "确认",
"labels.name": "名称",
"labels.name": "队伍名称",
"labels.bio": "简介",
"labels.logo": "",
"labels.banner": "",
"labels.link": "",
"labels.tag": "",
"labels.teamBsky": "",
"labels.teamEditor": "",
"labels.teamMemberRole": "",
"labels.teamMemberCustomRole": "",
"labels.teamMemberRoleType": "",
"labels.clockFormat": "",
"labels.disableBuildAbilitySorting": "",
"labels.disallowScrimPickupsFromUntrusted": "",
"labels.noScreen": "",
"labels.spoilerFreeMode": "",
"bottomTexts.imageModeration": "",
"bottomTexts.name": "请注意,如果您改了队名,那么其他人便可以使用之前的队名和URL。",
"bottomTexts.tag": "",
"bottomTexts.disableBuildAbilitySorting": "",
"bottomTexts.disallowScrimPickupsFromUntrusted": "",
"bottomTexts.noScreen": "",
"bottomTexts.spoilerFreeMode": "",
"bottomTexts.languageClockTimeNote": "",
"options.clockFormat.auto": "",
"options.clockFormat.24h": "",
"options.clockFormat.12h": "",
"labels.logo": "队徽",
"labels.banner": "横幅",
"labels.link": "链接",
"labels.tag": "标签",
"labels.teamBsky": "队伍 Bluesky",
"labels.teamEditor": "队伍管理",
"labels.teamMemberRole": "职责",
"labels.teamMemberCustomRole": "自定义职责",
"labels.teamMemberRoleType": "职责类型",
"labels.clockFormat": "时间格式",
"labels.disableBuildAbilitySorting": "配装:禁用技能自动排序",
"labels.disallowScrimPickupsFromUntrusted": "对抗战:禁止非好友将我加入临时队伍",
"labels.noScreen": "【无障碍】避开含有“浮墨幕墙”的比赛",
"labels.spoilerFreeMode": "隐藏赛事结果模式",
"bottomTexts.imageModeration": "除非您是赞助者,否则新上传的图片只有在管理员审核通过后才会公开显示。",
"bottomTexts.name": "请注意,如果您改了您的队伍名称,其他队伍将可以使用该名称和 URL。",
"bottomTexts.tag": "通常用在游戏内名字前,以表示所属队伍(例如:[TAG] 玩家名)。",
"bottomTexts.disableBuildAbilitySorting": "在您的个人主页之外,配装技能会自动排序,以便使相同的技能排列在一起。开启此设置后,您在所有页面看到的技能都将保持其最初创建时的原始顺序。",
"bottomTexts.disallowScrimPickupsFromUntrusted": "仅当您作为小组队长位于大厅中时生效。其他小组队长仍然可以拉您进房。",
"bottomTexts.noScreen": "此设置将应用于赛事、对抗战和 SendouQ。",
"bottomTexts.spoilerFreeMode": "隐藏过去一周内的赛事结果。",
"bottomTexts.languageClockTimeNote": "时钟和时间格式将根据您的浏览器语言设置显示。",
"options.clockFormat.auto": "自动",
"options.clockFormat.24h": "24 小时制",
"options.clockFormat.12h": "12 小时制",
"options.teamMemberRole.CAPTAIN": "队长",
"options.teamMemberRole.CO_CAPTAIN": "",
"options.teamMemberRole.CO_CAPTAIN": "副队长",
"options.teamMemberRole.FRONTLINE": "前排",
"options.teamMemberRole.SLAYER": "",
"options.teamMemberRole.SKIRMISHER": "",
"options.teamMemberRole.SLAYER": "输出",
"options.teamMemberRole.SKIRMISHER": "游击",
"options.teamMemberRole.SUPPORT": "辅助",
"options.teamMemberRole.MIDLINE": "中排",
"options.teamMemberRole.BACKLINE": "后排",
"options.teamMemberRole.FLEX": "自由",
"options.teamMemberRole.SUB": "",
"options.teamMemberRole.FLEX": "自由",
"options.teamMemberRole.SUB": "替补",
"options.teamMemberRole.COACH": "教练",
"options.teamMemberRole.CHEERLEADER": "",
"options.teamMemberRole.CUSTOM": "",
"options.teamMemberRoleType.PLAYER": "",
"options.teamMemberRoleType.OTHER": "",
"errors.required": "",
"errors.minLength": "",
"errors.maxLength": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
"errors.duplicateName": "该队已被使用",
"errors.duplicateOrgName": "",
"errors.noOnlySpecialCharacters": "",
"errors.customRoleRequired": "",
"errors.customRoleTypeRequired": "",
"errors.customRoleOnlyWhenCustom": "",
"options.teamMemberRole.CHEERLEADER": "应援",
"options.teamMemberRole.CUSTOM": "自定义...",
"options.teamMemberRoleType.PLAYER": "队员",
"options.teamMemberRoleType.OTHER": "其他",
"errors.required": "此项为必填项",
"errors.minLength": "字数不能少于 {{min}} 个字符",
"errors.maxLength": "字数不能多于 {{max}} 个字符",
"errors.invalidUrl": "请输入有效的 URL 地址",
"errors.notAllowedCharacters": "包含非法字符",
"errors.atLeastOneOption": "请至少选择一个选项",
"errors.duplicateName": "该队伍名称已被用",
"errors.duplicateOrgName": "该组织名称已被占用",
"errors.noOnlySpecialCharacters": "名称不能仅由特殊字符组成",
"errors.customRoleRequired": "请输入自定义职责的名称",
"errors.customRoleTypeRequired": "请选择自定义职责的类型",
"errors.customRoleOnlyWhenCustom": "仅当职责设为“自定义”时,才允许配置自定义职责",
"labels.weaponPool": "武器池",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
"options.voiceChat.yes": "",
"options.voiceChat.no": "",
"options.voiceChat.listenOnly": "",
"placeholders.weaponPoolFull": "武器池已满。请移除一个武器以添加新武器",
"labels.voiceChat": "可以进行语音聊天吗?",
"labels.languages": "您的语言",
"options.voiceChat.yes": "",
"options.voiceChat.no": "",
"options.voiceChat.listenOnly": "仅收听",
"labels.buildTitle": "标题",
"labels.buildModes": "模式",
"labels.buildPrivate": "",
"labels.buildPrivate": "私人",
"labels.buildWeapons": "武器",
"bottomTexts.buildPrivate": "私人配装仅对您可见",
"modes.TW": "",
"modes.SZ": "",
"modes.TC": "",
"modes.RM": "",
"modes.CB": "",
"errors.gearAllOrNone": "",
"labels.isEstablished": "",
"labels.text": "",
"bottomTexts.modNote": "",
"bottomTexts.scrimStart": "",
"labels.scrimStartFlexibility": "",
"bottomTexts.scrimStartFlexibility": "",
"labels.scrimManagedByAnyone": "",
"bottomTexts.scrimManagedByAnyone": "",
"labels.castTwitchAccounts": "",
"placeholders.castTwitchAccounts": "",
"bottomTexts.castTwitchAccounts": "",
"labels.scrimMaps": "",
"labels.scrimMaxDiv": "",
"labels.scrimMinDiv": "",
"labels.scrimMapSource": "",
"labels.scrimMapPool": "",
"labels.scrimMapsTournament": "",
"placeholders.scrimMapPool": "",
"options.scrimMapSource.POOL": "",
"options.scrimMapSource.TOURNAMENT": "",
"options.scrimFlexibility.notFlexible": "",
"options.scrimFlexibility.+30min": "",
"options.scrimFlexibility.+1hour": "",
"options.scrimFlexibility.+1.5hours": "",
"options.scrimFlexibility.+2hours": "",
"options.scrimFlexibility.+2.5hours": "",
"options.scrimFlexibility.+3hours": "",
"options.scrimMaps.noPreference": "",
"options.scrimMaps.szOnly": "",
"options.scrimMaps.rankedOnly": "",
"options.scrimMaps.allModes": "",
"options.scrimMaps.tournament": "",
"labels.scrimRequestMessage": "",
"labels.scrimRequestStartTime": "",
"bottomTexts.scrimRequestStartTime": "",
"errors.dateInPast": "",
"errors.dateTooEarly": "",
"errors.dateTooLate": "",
"errors.dateTooFarInFuture": "",
"errors.minUsersExcludingYourself": "",
"errors.usersMustBeUnique": "",
"errors.staffCannotBeAuthor": "",
"errors.divBothOrNeither": "",
"errors.invalidMapPool": "",
"errors.scrimTournamentRequired": "",
"errors.tournamentMustBeSelected": "",
"errors.tournamentOnlyWhenMapsIsTournament": "",
"errors.visibilityMustBeDifferent": "",
"errors.visibilityNotAllowedWhenPublic": "",
"errors.dateAfterScrimDate": "",
"errors.canNotSetIfLookingNow": "",
"errors.maxAssociationsReached": "",
"labels.weekdayTimes": "",
"labels.weekendTimes": "",
"labels.start": "",
"labels.end": "",
"labels.member": "",
"labels.members": "",
"labels.urls": "",
"modes.TW": "占地对战",
"modes.SZ": "真格区域",
"modes.TC": "真格塔楼",
"modes.RM": "真格鱼虎对战",
"modes.CB": "真格蛤蜊",
"errors.gearAllOrNone": "请将所有装备槽都填满或留空",
"labels.isEstablished": "是常设赛事组织",
"labels.text": "文本",
"bottomTexts.modNote": "此备注仅对工作人员可见",
"bottomTexts.scrimStart": "如果想立刻寻找对抗战,请保持默认",
"labels.scrimStartFlexibility": "弹性开始时间",
"bottomTexts.scrimStartFlexibility": "设置后,自开始时间后的此时间段内都允许对方申请",
"labels.scrimManagedByAnyone": "任何人均可编辑",
"bottomTexts.scrimManagedByAnyone": "如果启用,则不仅仅是创建者,此招募帖中的所有用户都可以接受申请和删除帖子。",
"labels.castTwitchAccounts": "Twitch 账户",
"placeholders.castTwitchAccounts": "dappleproductions",
"bottomTexts.castTwitchAccounts": "转播该赛事的 Twitch 账号。通过其个人资料,玩家直播将被自动创建。",
"labels.scrimMaps": "场地",
"labels.scrimMaxDiv": "级别上限",
"labels.scrimMinDiv": "级别下限",
"labels.scrimMapSource": "来源",
"labels.scrimMapPool": "地图池",
"labels.scrimMapsTournament": "赛事",
"placeholders.scrimMapPool": "https://sendou.ink/maps?pool=sz%3A3ffffff%3Btc%3A3555555",
"options.scrimMapSource.POOL": "地图池 URL",
"options.scrimMapSource.TOURNAMENT": "赛事",
"options.scrimFlexibility.notFlexible": "固定时间",
"options.scrimFlexibility.+30min": "+30 分钟",
"options.scrimFlexibility.+1hour": "+1 小时",
"options.scrimFlexibility.+1.5hours": "+1.5 小时",
"options.scrimFlexibility.+2hours": "+2 小时",
"options.scrimFlexibility.+2.5hours": "+2.5 小时",
"options.scrimFlexibility.+3hours": "+3 小时",
"options.scrimMaps.noPreference": "无偏好",
"options.scrimMaps.szOnly": "仅限真格区域",
"options.scrimMaps.rankedOnly": "仅限蛮颓比赛模式",
"options.scrimMaps.allModes": "全部模式",
"options.scrimMaps.tournament": "赛事...",
"labels.scrimRequestMessage": "消息",
"labels.scrimRequestStartTime": "开始时间",
"bottomTexts.scrimRequestStartTime": "请在招募帖的时间范围内选择一个时间",
"errors.dateInPast": "日期不能早于当前时间",
"errors.dateTooEarly": "日期过早",
"errors.dateTooLate": "日期过晚",
"errors.dateTooFarInFuture": "日期不能晚于当前时间超过 2 周",
"errors.minUsersExcludingYourself": "除您自己外,至少需要包含 {{min}} 位用户",
"errors.usersMustBeUnique": "用户不能重复",
"errors.staffCannotBeAuthor": "该赛事的创建者已经是组织者了",
"errors.divBothOrNeither": "最低级别和最高级别必须同时设置或同时不设置",
"errors.invalidMapPool": "无效的地图池",
"errors.scrimTournamentRequired": "请选择一项赛事",
"errors.tournamentMustBeSelected": "当地图池来源为赛事时,必须选择一项赛事",
"errors.tournamentOnlyWhenMapsIsTournament": "只有当地图池来源为赛事时,才可以选择赛事",
"errors.visibilityMustBeDifferent": "未找到状态下的可见性必须与基础可见性不同",
"errors.visibilityNotAllowedWhenPublic": "如果基础可见性已设为公开,则无法设置未找到状态下的可见性",
"errors.dateAfterScrimDate": "日期不能晚于对抗战日期",
"errors.canNotSetIfLookingNow": "如果当前正在寻找对抗战,则无法进行此设置",
"errors.maxAssociationsReached": "您已达到群组数量的上限",
"labels.weekdayTimes": "工作日",
"labels.weekendTimes": "周末",
"labels.start": "开始",
"labels.end": "结束",
"labels.member": "成员",
"labels.members": "成员",
"labels.urls": "URL",
"labels.description": "简介",
"labels.user": "用户",
"labels.orgMemberRole": "身份",
"labels.orgMemberRoleDisplayName": "身份显示名",
"labels.orgSocialLinks": "社交账号",
"labels.orgSeries": "系列",
"labels.orgSeriesName": "系列名称",
"labels.orgMemberRole": "职位",
"labels.orgMemberRoleDisplayName": "职位显示名",
"labels.orgSocialLinks": "社交媒体链接",
"labels.orgSeries": "系列",
"labels.orgSeriesName": "系列名称",
"labels.orgSeriesShowLeaderboard": "显示排行榜",
"labels.orgBadges": "徽章",
"bottomTexts.orgMembersInfo": "",
"options.orgRole.ADMIN": "管理",
"bottomTexts.orgMembersInfo": "请将您自己设置为管理员,以保持对该组织的控制权",
"options.orgRole.ADMIN": "管理",
"options.orgRole.MEMBER": "成员",
"options.orgRole.ORGANIZER": "组织者",
"options.orgRole.STREAMER": "直播者",
"labels.staff": "",
"labels.staffRole": "",
"bottomTexts.staffRolesInfo": "",
"options.staffRole.ORGANIZER": "",
"options.staffRole.STREAMER": "",
"labels.vodYoutubeUrl": "YouTube链接",
"options.orgRole.STREAMER": "主播",
"labels.staff": "工作人员",
"labels.staffRole": "职责",
"bottomTexts.staffRolesInfo": "组织者拥有与您相同的权限,但他们无法添加或移除工作人员。主播仅能在聊天频道中发言,并查看房间密码或频道。",
"options.staffRole.ORGANIZER": "组织者",
"options.staffRole.STREAMER": "主播",
"labels.vodYoutubeUrl": "YouTube URL",
"labels.vodTitle": "视频标题",
"labels.vodDate": "视频日期",
"labels.vodTeamSize": "",
"labels.vodTeamSize": "对战人数",
"labels.vodStartTimestamp": "从该时间戳开始",
"labels.vodMode": "模式",
"labels.vodStage": "地",
"labels.vodStage": "地",
"labels.vodWeapon": "武器",
"labels.vodWeaponsTeamOne": "武器编成队伍1",
"labels.vodWeaponsTeamTwo": "武器编成队伍2",
"labels.vodMatches": "",
"errors.dateMustNotBeFuture": "",
"errors.dateTooOld": "",
"labels.vodWeaponsTeamOne": "武器编成(队伍 1",
"labels.vodWeaponsTeamTwo": "武器编成(队伍 2",
"labels.vodMatches": "比赛",
"errors.dateMustNotBeFuture": "日期不能晚于当前时间",
"errors.dateTooOld": "日期必须晚于斯普拉遁 1 发售日2015 年 5 月 28 日)",
"vodTypes.TOURNAMENT": "大会(玩家视角)",
"vodTypes.CAST": "大会(观战视角)",
"vodTypes.SCRIM": "对抗战",
"vodTypes.MATCHMAKING": "蛮颓比赛/X比赛/占地对战",
"vodTypes.MATCHMAKING": "蛮颓比赛 / X比赛 / 占地对战",
"vodTypes.SENDOUQ": "SendouQ",
"labels.modesExact": "",
"bottomTexts.modesExact": "",
"labels.games": "",
"labels.vs": "",
"labels.startTime": "",
"labels.tagsIncluded": "",
"labels.tagsExcluded": "",
"labels.onlySendouEvents": "",
"labels.onlyRankedEvents": "",
"labels.minTeamCount": "",
"labels.orgsIncluded": "",
"labels.orgsExcluded": "",
"labels.authorIdsExcluded": "",
"bottomTexts.authorIdsExcluded": "",
"options.startTime.any": "",
"options.startTime.eu": "",
"options.startTime.na": "",
"options.startTime.au": "",
"options.game.S1": "",
"options.game.S2": "",
"options.game.S3": "",
"options.tag.SPECIAL": "",
"options.tag.ART": "",
"options.tag.MONEY": "",
"options.tag.REGION": "",
"options.tag.LOW": "",
"options.tag.HIGH": "",
"options.tag.COUNT": "",
"options.tag.LAN": "",
"options.tag.QUALIFIER": "",
"options.tag.COLLEGIATE": "",
"options.tag.ONES": "",
"options.tag.DUOS": "",
"options.tag.TRIOS": "",
"options.tag.S1": "",
"options.tag.S2": "",
"options.tag.SR": "",
"options.tag.CARDS": "",
"labels.player": "",
"labels.banUserNote": "",
"bottomTexts.banUserNoteHelp": "",
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.scrimCancelReason": "",
"bottomTexts.scrimCancelReasonHelp": "",
"bottomTexts.bioMarkdown": "",
"labels.division": "",
"options.division.both": "",
"options.division.tentatek": "",
"options.division.takoroka": "",
"labels.timezone": "",
"labels.favoriteStage": "",
"labels.peakXp": "",
"labels.weapon": "",
"labels.artSource": "",
"options.artSource.ALL": "",
"options.artSource.MADE-BY": "",
"options.artSource.MADE-OF": "",
"labels.tierListUrl": "",
"labels.plusTier": "",
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.modesExact": "精确筛选",
"bottomTexts.modesExact": "仅显示完全符合选定模式的赛事",
"labels.games": "游戏",
"labels.vs": "对战人数",
"labels.startTime": "开始时间",
"labels.tagsIncluded": "包含的标签",
"labels.tagsExcluded": "排除的标签",
"labels.onlySendouEvents": "仅在 sendou.ink 举办的赛事",
"labels.onlyRankedEvents": "仅限排位赛事",
"labels.minTeamCount": "参赛队伍数下限",
"labels.orgsIncluded": "显示的组织",
"labels.orgsExcluded": "隐藏的组织",
"labels.authorIdsExcluded": "排除的创建者",
"bottomTexts.authorIdsExcluded": "您可以在用户的个人资料页找到他们的 ID",
"options.startTime.any": "任意",
"options.startTime.eu": "适合欧洲时间",
"options.startTime.na": "适合美洲时间",
"options.startTime.au": "适合澳洲 / 新加坡时间",
"options.game.S1": "斯普拉遁 1",
"options.game.S2": "斯普拉遁 2",
"options.game.S3": "斯普拉遁 3",
"options.tag.SPECIAL": "特殊规则",
"options.tag.ART": "插画奖励",
"options.tag.MONEY": "奖金",
"options.tag.REGION": "限制地区",
"options.tag.LOW": "水平上限",
"options.tag.HIGH": "水平下限",
"options.tag.COUNT": "限制队伍数量",
"options.tag.LAN": "线下",
"options.tag.QUALIFIER": "资格赛",
"options.tag.COLLEGIATE": "高校",
"options.tag.ONES": "1v1",
"options.tag.DUOS": "2v2",
"options.tag.TRIOS": "3v3",
"options.tag.S1": "斯普拉遁 1",
"options.tag.S2": "斯普拉遁 2",
"options.tag.SR": "鲑鱼跑",
"options.tag.CARDS": "占地斗士",
"labels.player": "玩家",
"labels.banUserNote": "内部备注",
"bottomTexts.banUserNoteHelp": "此备注仅对组织管理员可见。",
"labels.banUserExpiresAt": "封禁截止日期",
"bottomTexts.banUserExpiresAtHelp": "留空则表示永久封禁",
"labels.scrimCancelReason": "取消原因",
"bottomTexts.scrimCancelReasonHelp": "请说明取消这场对抗战的原因。该内容将对另一支队伍公开。",
"bottomTexts.bioMarkdown": "支持 Markdown 语法",
"labels.division": "组别",
"options.division.both": "均可",
"options.division.tentatek": "仅限艾洛眼",
"options.division.takoroka": "仅限暇古",
"labels.timezone": "时区",
"labels.favoriteStage": "喜爱的场地",
"labels.peakXp": "最高 XP",
"labels.weapon": "武器",
"labels.artSource": "作品来源",
"options.artSource.ALL": "全部",
"options.artSource.MADE-BY": "由我创作",
"options.artSource.MADE-OF": "与我相关",
"labels.tierListUrl": "强度榜 URL",
"labels.plusTier": "级别",
"labels.comment": "评论",
"errors.plusAlreadySuggested": "此用户已被推荐过",
"errors.plusAlreadyMember": "此用户已经在当前级别了",
"errors.plusCannotSuggest": "当前无法进行推荐",
"labels.profileCustomAvatar": "自定义头像",
"labels.profileCustomName": "自定义昵称",
"labels.profileCustomUrl": "自定义URL",
"labels.inGameName": "游戏ID",
"labels.profileBattlefy": "Battlefy用户名",
"labels.profileMotionSens": "体感感度",
"labels.profileStickSens": "摇杆度",
"labels.profileCountry": "国家",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "显示Discord用户名",
"labels.profileCommissionsOpen": "开放委托",
"labels.profileCustomUrl": "自定义 URL",
"labels.inGameName": "游戏内昵称",
"labels.profileBattlefy": "Battlefy 用户名",
"labels.profileMotionSens": "陀螺仪操作灵敏度",
"labels.profileStickSens": "摇杆操作灵敏度",
"labels.profileCountry": "国家 / 地区",
"labels.profileFavoriteBadges": "喜爱的徽章",
"labels.profileShowDiscordUniqueName": "显示 Discord 用户名",
"labels.profileCommissionsOpen": "委托开放中",
"labels.profileCommissionText": "委托信息",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "如果此栏空着,将会显示您的Discord昵称:\"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "Battlefy用户名会在部分比赛被用于种子排名和验证",
"bottomTexts.profileShowDiscordUniqueName": "是否公开显示您的Discord用户名 ({{discordUniqueName}}) ",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "你的价格、档期或者其他委托相关信息",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "自定义URL能包含特殊符号",
"errors.profileCustomUrlNumbers": "自定义URL不能只包含数字",
"errors.profileCustomUrlDuplicate": "这个自定义URL已被使用",
"errors.profileSensBothOrNeither": "设置体感感度前请先设置摇杆感度",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",
"labels.friendUser": "",
"errors.cannotFriendSelf": "",
"errors.alreadyFriends": "",
"errors.friendRequestExists": "",
"labels.type": "",
"labels.visibility": "",
"labels.note": "",
"labels.stayAsSub": "",
"bottomTexts.stayAsSub": "",
"labels.regLinkedTeam": "",
"labels.regTeamName": "",
"labels.regTeam": "",
"labels.regCaptain": "",
"labels.regSignUpAs": "",
"labels.regPickUpName": "",
"labels.regPrefersNotToHost": "",
"errors.regLinkedTeamRequired": "",
"errors.regTeamNameRequired": "",
"errors.regOwnerMustBeMember": "",
"errors.regTeamNameTaken": "",
"errors.regTooManyMembers": "",
"errors.regMemberInvalid": "",
"errors.regMemberNoFriendCode": "",
"errors.regMemberNoInGameName": "",
"errors.regMemberBanned": "",
"errors.regMemberOnAnotherTeam": "",
"errors.regCannotRemoveParticipatedPlayer": "",
"errors.regCheckedInBelowMinRoster": "",
"regImportTeam": "",
"labels.regImportSourceTournament": "",
"errors.regImportTournamentRequired": ""
"labels.profileNewProfileEnabled": "新个人资料页",
"bottomTexts.profileCustomAvatar": "赞助者Supporter 及以上级别)可以上传自定义图片来替代 Discord 头像",
"bottomTexts.profileCustomName": "如果留空,系统将直接显示您的 Discord 用户名",
"bottomTexts.profileCustomUrl": "赞助者Supporter 及以上级别)可以使用短链接。例如:您可以使用 snd.ink/sendou 来替代 sendou.ink/u/sendou。",
"bottomTexts.profileInGameName": "格式:游戏内名称#数字编号例如Player#1234",
"bottomTexts.profileBattlefy": "在部分赛事中会被用于种子排名和身份验证",
"bottomTexts.profileShowDiscordUniqueName": "在个人资料上公开显示您的 Discord 用户名",
"bottomTexts.profileCommissionsOpen": "委托开放状态将在一个月后自动关闭",
"bottomTexts.profileCommissionText": "价格、剩余名额或其他约稿相关信息",
"bottomTexts.profileNewProfileEnabled": "启用基于小组件的全新个人主页(仅限赞助者可用)",
"errors.profileCustomUrlStrangeChar": "自定义 URL能包含字母、数字、连字符 (-) 和下划线 (_)",
"errors.profileCustomUrlNumbers": "自定义 URL 不能只由纯数字组成",
"errors.profileCustomUrlDuplicate": "自定义 URL 已被其他人使用",
"errors.profileSensBothOrNeither": "如果未设置右摇杆操作灵敏度,则无法单独设置陀螺仪操作灵敏度",
"errors.profileInGameName": "必须符合此格式:名称#数字编号1-10 位字符,紧跟 #,再加 4-5 位英文 / 数字)",
"inGameName.addCharacter": "添加特殊字符",
"inGameName.categories.symbols": "符号",
"inGameName.categories.accented": "带变音符号字母",
"inGameName.categories.greek": "希腊字母",
"inGameName.categories.cyrillic": "西里尔字母",
"inGameName.categories.hiragana": "平假名",
"inGameName.categories.katakana": "片假名",
"labels.pronoun": "代词",
"bottomTexts.profilePronouns": "此设置是可选的您的代词将显示在您的个人资料、赛事名册、SendouQ 小组以及文字频道中。",
"errors.profilePronounsBothOrNeither": "请同时选择两个代词,或者均不选择",
"labels.friendUser": "用户",
"errors.cannotFriendSelf": "您不能向自己发送好友请求",
"errors.alreadyFriends": "您与该用户已经是好友了",
"errors.friendRequestExists": "您与该用户之间已存在好友请求",
"labels.type": "类型",
"labels.visibility": "可见性",
"labels.note": "备注",
"labels.stayAsSub": "保持为替补",
"bottomTexts.stayAsSub": "如果您在报名结束前未能找到队伍成员,您将在本次赛事期间保持为单人替补状态",
"labels.regLinkedTeam": "关联至 sendou.ink 队伍",
"labels.regTeamName": "队伍名称",
"labels.regTeam": "队伍",
"labels.regCaptain": "队长",
"labels.regSignUpAs": "队伍报名形式",
"labels.regPickUpName": "临时队伍名称",
"labels.regPrefersNotToHost": "我方队伍不倾向于担任房主",
"errors.regLinkedTeamRequired": "请选择一支队伍",
"errors.regTeamNameRequired": "队伍名称不能为空",
"errors.regOwnerMustBeMember": "队长必须是参赛成员之一",
"errors.regTeamNameTaken": "该队伍名称已被占用",
"errors.regTooManyMembers": "名单中的玩家数量过多",
"errors.regMemberInvalid": "无效的玩家",
"errors.regMemberNoFriendCode": "该玩家未设置好友编号",
"errors.regMemberNoInGameName": "该玩家未设置游戏内昵称",
"errors.regMemberBanned": "该玩家目前已被 sendou.ink 封禁",
"errors.regMemberOnAnotherTeam": "该玩家已经加入了其他队伍",
"errors.regCannotRemoveParticipatedPlayer": "无法移除已在赛事中上场过的玩家",
"errors.regCheckedInBelowMinRoster": "已签到的队伍,其名单人数不能低于最低限制",
"regImportTeam": "导入队伍",
"labels.regImportSourceTournament": "赛事",
"errors.regImportTournamentRequired": "请选择一个赛事"
}

View File

@@ -1,19 +1,19 @@
{
"incomingRequests.title": "",
"sendRequest.title": "",
"sendRequest.submit": "",
"pendingRequests.title": "",
"friendsList.title": "",
"friendsList.empty": "",
"friendsList.friendSince": "",
"friendsList.viewUserPage": "",
"friendsList.viewTournament": "",
"friendsList.joinSendouQ": "",
"friendsList.deleteFriend": "",
"friendsList.deleteConfirm": "",
"view.label": "",
"view.friends": "",
"view.teamMembers": "",
"view.all": "",
"teamMembers.empty": ""
"incomingRequests.title": "收到的请求",
"sendRequest.title": "发送好友请求",
"sendRequest.submit": "发送请求",
"pendingRequests.title": "待处理的请求",
"friendsList.title": "好友列表",
"friendsList.empty": "暂无好友",
"friendsList.friendSince": "于 {{date}} 成为好友",
"friendsList.viewUserPage": "查看用户主页",
"friendsList.viewTournament": "查看赛事",
"friendsList.joinSendouQ": "加入 SendouQ",
"friendsList.deleteFriend": "删除好友",
"friendsList.deleteConfirm": "确定要删除好友 {{name}} 吗?",
"view.label": "筛选显示",
"view.friends": "好友",
"view.teamMembers": "队伍成员",
"view.all": "全部",
"teamMembers.empty": "暂无队伍成员"
}

View File

@@ -10,6 +10,7 @@
"showcase.card.unranked": "非排位",
"showcase.card.winner": "获胜者",
"showcase.results": "近期结果",
"showcase.viewAll": "",
"leaderboards.topPlayers": "顶级玩家",
"leaderboards.topTeams": "顶级队伍",
"leaderboards.viewFull": "查看完整排行榜",
@@ -39,7 +40,7 @@
"rotations.current": "目前",
"rotations.nextLabel": "下次",
"rotations.credit": "数据来自 splatoon3.ink",
"rotations.filter.all": "所有",
"rotations.filter.all": "全部",
"discover.header": "探索全部功能",
"install.button": "安装应用",
"install.header": "安装 sendou.ink",

View File

@@ -18,13 +18,13 @@
"filters.MinTier": "SendouQ 段位下限",
"filters.suffix": "筛选条件",
"filters.orAbove": "或以上",
"new.noMorePosts": "您不能再发表更多招募信息了。",
"new.noMorePosts": "您不能再发表更多招募了。",
"new.type.header": "类型",
"new.timezone.header": "时区",
"new.text.header": "文字",
"new.visibility.header": "可见范围",
"new.visibility.everyone": "所有人",
"new.editOn": "由您编辑",
"new.editOn": "在这里编辑: ",
"new.weaponPool.header": "武器池",
"new.weaponPool.userProfile": "用户主页",
"new.languages.header": "语言",

View File

@@ -1,120 +1,120 @@
{
"tabs.owned": "",
"tabs.booked": "",
"tabs.available": "",
"now": "",
"noneAvailable": "",
"noRequestsYet": "",
"noOwnedPosts": "",
"noBookedScrims": "",
"requestModal.title": "",
"requestModal.message.label": "",
"requestModal.at.label": "",
"requestModal.at.explanation": "",
"pickupBy": "",
"filters.button": "",
"filters.heading": "",
"filters.weekdayTimes": "",
"filters.weekdayStart": "",
"filters.weekdayEnd": "",
"filters.weekendTimes": "",
"filters.weekendStart": "",
"filters.weekendEnd": "",
"filters.apply": "",
"filters.applyAndDefault": "",
"filters.showFiltered": "",
"filters.hideFiltered": "",
"filters.showPendingRequests": "",
"filters.hidePendingRequests": "",
"limitedVisibility": "",
"actions.request": "",
"actions.viewRequest": "",
"actions.contact": "",
"deleteModal.title": "",
"cancelRequestModal.title": "",
"cancelModal.scrim.title": "",
"cancelModal.scrim.reasonLabel": "",
"cancelModal.scrim.reasonExplanation": "",
"acceptModal.title": "",
"acceptModal.prevented": "",
"acceptModal.confirmFor": "",
"autoCancelInfo": "",
"postModal.footer": "",
"forms.title": "",
"forms.with.title": "",
"forms.with.explanation": "",
"forms.with.user": "",
"forms.with.pick-up": "",
"forms.when.title": "",
"forms.when.explanation": "",
"forms.rangeEnd.title": "",
"forms.rangeEnd.explanation": "",
"forms.rangeEnd.notFlexible": "",
"forms.rangeEnd.+30min": "",
"forms.rangeEnd.+1hour": "",
"forms.rangeEnd.+1.5hours": "",
"forms.rangeEnd.+2hours": "",
"forms.rangeEnd.+2.5hours": "",
"forms.rangeEnd.+3hours": "",
"forms.text.title": "",
"forms.visibility.title": "",
"forms.visibility.public": "",
"forms.visibility.friends": "",
"forms.visibility.noneAvailable": "",
"forms.notFoundVisibility.title": "",
"forms.notFoundVisibility.explanation": "",
"forms.divs.minDiv.title": "",
"forms.divs.maxDiv.title": "",
"forms.maps.title": "",
"forms.maps.noPreference": "",
"forms.maps.szOnly": "",
"forms.maps.rankedOnly": "",
"forms.maps.allModes": "",
"forms.maps.tournament": "",
"forms.mapsTournament.title": "",
"page.scheduledScrim": "",
"page.vs": "",
"associations.title": "",
"associations.explanation": "",
"associations.join.title": "",
"associations.delete.title": "",
"associations.admin": "",
"associations.leave.title": "",
"associations.leave.action": "",
"associations.shareLink.title": "",
"associations.shareLink.reset": "",
"associations.removeMember.title": "",
"associations.forms.title": "",
"associations.forms.name.title": "",
"forms.managedByAnyone.title": "",
"forms.managedByAnyone.explanation": "",
"banner.canceled.header": "",
"banner.canceled.subtitle": "",
"banner.freeForm.header": "",
"banner.freeForm.subtitle": "",
"mapByMap.nonParticipantNotice": "",
"mapByMap.noCurrentMap": "",
"mapByMap.undo": "",
"mapByMap.replay": "",
"mapByMap.pick": "",
"mapByMap.pickDialog.heading": "",
"mapByMap.removeList": "",
"mapByMap.removeListConfirm": "",
"mapByMap.submitListHeading": "",
"mapByMap.noListYet": "",
"mapByMap.manageMapLists": "",
"mapByMap.poolList": "",
"mapByMap.result.replayTag": "",
"mapByMap.stats.empty": "",
"mapByMap.stats.restrictToPool": "",
"mapByMap.stats.byMode": "",
"mapByMap.stats.byStage": "",
"mapByMap.stats.byStageMode": "",
"mapByMap.stats.view.MODE": "",
"mapByMap.stats.view.STAGE": "",
"mapByMap.stats.view.BOTH": "",
"mapByMap.stats.col.label": "",
"mapByMap.stats.col.wins": "",
"mapByMap.stats.col.losses": "",
"mapByMap.stats.col.winPct": ""
"tabs.owned": "我的发布",
"tabs.booked": "已预约",
"tabs.available": "可加入",
"now": "现在",
"noneAvailable": "目前没有可加入的对抗战。请稍后再试,或自己发布一个!",
"noRequestsYet": "暂无请求",
"noOwnedPosts": "您目前没有处于开启状态的对抗战招募帖",
"noBookedScrims": "暂无已预约的对抗战",
"requestModal.title": "发送对抗战请求",
"requestModal.message.label": "留言",
"requestModal.at.label": "开始时间",
"requestModal.at.explanation": "请在招募帖的时间范围内选择一个时间",
"pickupBy": "临时队员:",
"filters.button": "筛选",
"filters.heading": "对抗战筛选",
"filters.weekdayTimes": "工作日时间",
"filters.weekdayStart": "工作日开始时间",
"filters.weekdayEnd": "工作日结束时间",
"filters.weekendTimes": "周末时间",
"filters.weekendStart": "周末开始时间",
"filters.weekendEnd": "周末结束时间",
"filters.apply": "应用",
"filters.applyAndDefault": "应用并设为默认",
"filters.showFiltered": "显示已过滤内容 ({{count}})",
"filters.hideFiltered": "隐藏已过滤内容 ({{count}})",
"filters.showPendingRequests": "显示待处理请求 ({{count}})",
"filters.hidePendingRequests": "隐藏待处理请求 ({{count}})",
"limitedVisibility": "由于群组限制,此招募帖目前仅限部分可见。",
"actions.request": "申请加入",
"actions.viewRequest": "申请处理中...",
"actions.contact": "联系对方",
"deleteModal.title": "删除此对抗战招募帖?",
"cancelRequestModal.title": "取消您的申请?",
"cancelModal.scrim.title": "取消这场对抗战?",
"cancelModal.scrim.reasonLabel": "取消原因",
"cancelModal.scrim.reasonExplanation": "请说明您取消对抗战的原因。此内容将对另一支队伍可见。",
"acceptModal.title": "接受 {{groupName}} 的对抗战申请并拒绝其他申请(如果有)?",
"acceptModal.prevented": "请联系发布此对抗战的人来接受该申请",
"acceptModal.confirmFor": "确认于 {{time}}",
"autoCancelInfo": "一旦对抗战成功预约,在 ±1 小时范围内含有相同队员的其他招募帖和申请都将被自动移除,以避免冲突。",
"postModal.footer": "招募帖创建于 {{time}}",
"forms.title": "创建新的对抗战招募帖",
"forms.with.title": "组队方式",
"forms.with.explanation": "您可以通过用户名、Discord ID 或 sendou.ink 的个人资料链接来搜索用户。",
"forms.with.user": "队员 {{nth}}",
"forms.with.pick-up": "临时队员",
"forms.when.title": "开始时间",
"forms.when.explanation": "如果想立刻寻找对抗战,请保持默认",
"forms.rangeEnd.title": "弹性开始时间",
"forms.rangeEnd.explanation": "设置后,自开始时间后的此时间段内都允许对方申请",
"forms.rangeEnd.notFlexible": "固定时间",
"forms.rangeEnd.+30min": "+30 分钟",
"forms.rangeEnd.+1hour": "+1 小时",
"forms.rangeEnd.+1.5hours": "+1.5 小时",
"forms.rangeEnd.+2hours": "+2 小时",
"forms.rangeEnd.+2.5hours": "+2.5 小时",
"forms.rangeEnd.+3hours": "+3 小时",
"forms.text.title": "备注",
"forms.visibility.title": "可见范围",
"forms.visibility.public": "公开",
"forms.visibility.friends": "仅限好友",
"forms.visibility.noneAvailable": "没有可用的群组。创建一个群组来在更小的圈子里寻找对抗战吧。",
"forms.notFoundVisibility.title": "若在指定时间后未匹配成功: ",
"forms.notFoundVisibility.explanation": "如果您不想让帖子的可见范围在超时后改变,请保持留空",
"forms.divs.minDiv.title": "级别下限",
"forms.divs.maxDiv.title": "级别上限",
"forms.maps.title": "对战场地与模式",
"forms.maps.noPreference": "无偏好",
"forms.maps.szOnly": "仅限真格区域",
"forms.maps.rankedOnly": "仅限蛮颓比赛模式",
"forms.maps.allModes": "全部模式",
"forms.maps.tournament": "赛事...",
"forms.mapsTournament.title": "赛事",
"page.scheduledScrim": "已安排到日程的对抗战",
"page.vs": "对战 {{opponent}}",
"associations.title": "群组",
"associations.explanation": "创建一个群组来在更小的圈子里寻找对手(例如,为您队伍的常约练习对手或 LUTI 联赛的分组创建一个群组)。",
"associations.join.title": "加入群组 {{name}} ",
"associations.delete.title": "删除群组 {{name}} ",
"associations.admin": "管理员: {{username}}",
"associations.leave.title": "退出群组 {{name}} ",
"associations.leave.action": "退出",
"associations.shareLink.title": "分享链接以邀请成员",
"associations.shareLink.reset": "重置链接",
"associations.removeMember.title": "将 {{username}} 从群组中移除?",
"associations.forms.title": "创建新群组",
"associations.forms.name.title": "群组名称",
"forms.managedByAnyone.title": "任何人均可编辑",
"forms.managedByAnyone.explanation": "如果启用,则不仅仅是创建者,此招募帖中的所有用户都可以接受申请和删除帖子。",
"banner.canceled.header": "由 {{user}} 取消",
"banner.canceled.subtitle": "原因:{{reason}}",
"banner.freeForm.header": "自由模式练习",
"banner.freeForm.subtitle": "设置一个场地池以开始抽取场地(可选)",
"mapByMap.nonParticipantNotice": "只有参赛人员可以管理场地追踪。",
"mapByMap.noCurrentMap": "正在等待生成下一个场地。",
"mapByMap.undo": "撤销",
"mapByMap.replay": "重赛",
"mapByMap.pick": "挑选场地",
"mapByMap.pickDialog.heading": "选择场地",
"mapByMap.removeList": "移除场地列表",
"mapByMap.removeListConfirm": "确定要移除您的场地列表吗?",
"mapByMap.submitListHeading": "提交您的场地列表",
"mapByMap.noListYet": "尚未提交",
"mapByMap.manageMapLists": "管理场地列表",
"mapByMap.poolList": "场地池 ({{count}} 个场地)",
"mapByMap.result.replayTag": "场地重赛 #{{index}}",
"mapByMap.stats.empty": "暂无历史场地数据报告",
"mapByMap.stats.restrictToPool": "仅限我提交的自定义场地池",
"mapByMap.stats.byMode": "按模式分类",
"mapByMap.stats.byStage": "按场地分类",
"mapByMap.stats.byStageMode": "按场地与模式分类",
"mapByMap.stats.view.MODE": "模式",
"mapByMap.stats.view.STAGE": "场地",
"mapByMap.stats.view.BOTH": "场地与模式",
"mapByMap.stats.col.label": "场地",
"mapByMap.stats.col.wins": "胜场",
"mapByMap.stats.col.losses": "败场",
"mapByMap.stats.col.winPct": "胜率"
}

View File

@@ -14,7 +14,7 @@
"matchProfile.weaponPool.header": "武器池",
"matchProfile.weaponPool.full": "武器池已满",
"matchProfile.voiceChat.header": "语音聊天",
"matchProfile.voiceChat.canVC.header": "可以进行语音聊天吗?",
"matchProfile.voiceChat.canVC.header": "可以进行语音聊天吗?",
"matchProfile.voiceChat.canVC.yes": "是",
"matchProfile.voiceChat.canVC.no": "否",
"matchProfile.voiceChat.canVC.listenOnly": "仅收听",

View File

@@ -31,6 +31,7 @@
"pre.footer": "在比赛开始前,报名信息可随意改动",
"pre.logIn": "登录以报名",
"pre.inATeam": "您已经在本次活动的某支队伍里了",
"pre.captainOnlyEdit": "",
"pre.registrationClosed": "",
"pre.checkIn.range": "签到开放时间为 {{start}} 至 {{finish}}",
"pre.checkIn.over": "签到已结束",

View File

@@ -1,42 +1,42 @@
{
"type.SCRIM": "对抗战",
"type.TOURNAMENT": "大会(玩家视角)",
"type.MATCHMAKING": "蛮颓比赛/X比赛/占地对战",
"type.CAST": "大会(观战视角)",
"type.TOURNAMENT": "赛事(玩家视角)",
"type.MATCHMAKING": "蛮颓比赛 / X比赛 / 占地对战",
"type.CAST": "赛事(观战视角)",
"type.SENDOUQ": "SendouQ",
"gameCount": "第 {{count}} 场比赛",
"minShort": "分",
"secShort": "秒",
"forms.title.create": "",
"forms.title.edit": "",
"forms.title.youtubeUrl": "YouTube链接",
"forms.title.create": "发布视频",
"forms.title.edit": "编辑视频",
"forms.title.youtubeUrl": "YouTube URL",
"forms.title.videoTitle": "视频标题",
"forms.title.videoDate": "视频日期",
"forms.title.type": "类型",
"forms.title.pov": "玩家(主视角",
"forms.title.startTimestamp": "从该时间开始",
"forms.title.pov": "玩家视角",
"forms.title.startTimestamp": "从该时间开始",
"forms.title.mode": "模式",
"forms.title.stage": "地",
"forms.title.weaponsTeamOne": "武器编成队伍1",
"forms.title.weaponsTeamTwo": "武器编成队伍2",
"forms.title.stage": "地",
"forms.title.weaponsTeamOne": "武器编成(队伍 1",
"forms.title.weaponsTeamTwo": "武器编成(队伍 2",
"forms.title.weapon": "武器",
"forms.title.teamSize": "",
"teamSize.1v1": "",
"teamSize.2v2": "",
"teamSize.3v3": "",
"teamSize.4v4": "",
"forms.title.teamSize": "对战人数",
"teamSize.1v1": "1v1",
"teamSize.2v2": "2v2",
"teamSize.3v3": "3v3",
"teamSize.4v4": "4v4",
"forms.action.addMatch": "添加比赛",
"forms.action.deleteMatch": "删除比赛",
"forms.action.setAsCurrent": "",
"forms.action.copyFromPrevious": "",
"noVods": "",
"gainPerms": "请在我们的Discord服务器的helpdesk频道申请获取上传视频的资格。",
"forms.action.setAsCurrent": "设为当前时间 ({{time}})",
"forms.action.copyFromPrevious": "从上个比赛复制",
"noVods": "未找到符合当前筛选条件的视频。我们遗漏了什么吗?请参阅常见问题与解答页面以获取有关如何获取视频上传权限的信息。",
"gainPerms": "请在本网站的 Discord 服务器的 helpdesk 频道申请获取上传视频的资格。",
"deleteConfirm": "确认要删除 '{{title}}' 吗?",
"errors.youtubePreviewFailed": "",
"copyTimestamps": "",
"copyTimestamps.help": "",
"copyTimestamps.modeFormat": "",
"copyTimestamps.stageFormat": "",
"copyTimestamps.format.short": "",
"copyTimestamps.format.long": ""
"errors.youtubePreviewFailed": "加载 YouTube 预览失败",
"copyTimestamps": "复制时间戳",
"copyTimestamps.help": "将这些内容粘贴到您的 YouTube 视频简介中,即可自动创建视频章节标记。",
"copyTimestamps.modeFormat": "模式格式",
"copyTimestamps.stageFormat": "场地格式",
"copyTimestamps.format.short": "短格式",
"copyTimestamps.format.long": "长格式"
}

View File

@@ -37,8 +37,8 @@
"knip": "knip"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1064.0",
"@aws-sdk/lib-storage": "3.1064.0",
"@aws-sdk/client-s3": "3.1065.0",
"@aws-sdk/lib-storage": "3.1065.0",
"@date-fns/tz": "1.5.0",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
@@ -51,7 +51,7 @@
"@react-router/node": "7.17.0",
"@react-router/serve": "7.15.0",
"@remix-run/form-data-parser": "0.17.3",
"@sentry/react-router": "^10.56.0",
"@sentry/react-router": "^10.57.0",
"@tldraw/tldraw": "3.12.1",
"@zumer/snapdom": "2.12.8",
"better-sqlite3": "12.10.0",
@@ -68,7 +68,7 @@
"jsoncrush": "1.1.8",
"kysely": "0.29.0",
"lucide-react": "1.17.0",
"markdown-to-jsx": "9.8.1",
"markdown-to-jsx": "9.8.2",
"nanoid": "5.1.11",
"neverthrow": "8.2.0",
"node-cron": "4.2.1",
@@ -86,7 +86,7 @@
"react-i18next": "17.0.8",
"react-router": "7.17.0",
"react-use-draggable-scroll": "0.4.7",
"remeda": "2.38.0",
"remeda": "2.39.0",
"remix-auth": "4.2.0",
"remix-auth-oauth2": "3.4.1",
"remix-i18next": "7.5.0",

222
pnpm-lock.yaml generated
View File

@@ -13,11 +13,11 @@ importers:
.:
dependencies:
'@aws-sdk/client-s3':
specifier: 3.1064.0
version: 3.1064.0
specifier: 3.1065.0
version: 3.1065.0
'@aws-sdk/lib-storage':
specifier: 3.1064.0
version: 3.1064.0(@aws-sdk/client-s3@3.1064.0)
specifier: 3.1065.0
version: 3.1065.0(@aws-sdk/client-s3@3.1065.0)
'@date-fns/tz':
specifier: 1.5.0
version: 1.5.0
@@ -55,8 +55,8 @@ importers:
specifier: 0.17.3
version: 0.17.3
'@sentry/react-router':
specifier: ^10.56.0
version: 10.56.0(@react-router/node@7.17.0(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3))(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rollup@4.62.0)
specifier: ^10.57.0
version: 10.57.0(@react-router/node@7.17.0(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3))(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rollup@4.62.0)
'@tldraw/tldraw':
specifier: 3.12.1
version: 3.12.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -106,8 +106,8 @@ importers:
specifier: 1.17.0
version: 1.17.0(react@19.2.7)
markdown-to-jsx:
specifier: 9.8.1
version: 9.8.1(react@19.2.7)
specifier: 9.8.2
version: 9.8.2(react@19.2.7)
nanoid:
specifier: 5.1.11
version: 5.1.11
@@ -160,8 +160,8 @@ importers:
specifier: 0.4.7
version: 0.4.7(react@19.2.7)
remeda:
specifier: 2.38.0
version: 2.38.0
specifier: 2.39.0
version: 2.39.0
remix-auth:
specifier: 4.2.0
version: 4.2.0
@@ -292,8 +292,8 @@ packages:
resolution: {integrity: sha512-RMCrCteiUwYTEv2G9zfP/BEuKHv57665vVieJyp9cf8VgilWxP/KrWVtMdfdDlIH8nFhvu3rIMc29z3ebGEZ1w==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-s3@3.1064.0':
resolution: {integrity: sha512-6OQhE4Qpt94oTw7ruBHE2E6/PS57alsCSdemMf4c3gBSUM0emoXRRykYffFSL56oluL49AZh/DLWRnQafynbLg==}
'@aws-sdk/client-s3@3.1065.0':
resolution: {integrity: sha512-KtCpg2vihUjhUpqpsZIcB6Dm1hv9lRn5hWwU6hXtYfSQionFD/dB/gTwgyn9AHX6eGiCFqHfjIZwETz5Cz4RWA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/core@3.974.21':
@@ -332,11 +332,11 @@ packages:
resolution: {integrity: sha512-JmMGlhVvSj8uSG9CpeDkJAXT35H89tc6v84iMgEIE75q4yp1MKVVKvopv6Gg28HJIR7hMNkojRF8H2m5W44wyg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/lib-storage@3.1064.0':
resolution: {integrity: sha512-dEUFZx29gRaKFlZB/7N1It8jVtaDY2x967s200xNnl//44Ak2/uK30OujL8tKzRwAz5M50/eNQozGw0jdES/pQ==}
'@aws-sdk/lib-storage@3.1065.0':
resolution: {integrity: sha512-0eEVoAWuvob9BkiKc/jtFPaOQYR7hbW6xioTe3b1U1WpwyVLKO6KROdm1zpMC7k5EOpVDhdFXkqZvFSCJuc4Uw==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@aws-sdk/client-s3': ^3.1064.0
'@aws-sdk/client-s3': ^3.1065.0
'@aws-sdk/middleware-flexible-checksums@3.974.31':
resolution: {integrity: sha512-Yzj6NRYVZdBaCp7o1BwHGyeDBfixdeToLIAMprshIITEdl9wKVSiidVOfeaiH8FyeC1hBmBfDZFvs/aH1Y3xpw==}
@@ -864,8 +864,8 @@ packages:
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
engines: {node: '>=8.0.0'}
'@opentelemetry/core@2.7.1':
resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==}
'@opentelemetry/core@2.8.0':
resolution: {integrity: sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
@@ -876,14 +876,14 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/resources@2.7.1':
resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==}
'@opentelemetry/resources@2.8.0':
resolution: {integrity: sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-trace-base@2.7.1':
resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==}
'@opentelemetry/sdk-trace-base@2.8.0':
resolution: {integrity: sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
@@ -1847,32 +1847,32 @@ packages:
cpu: [x64]
os: [win32]
'@sentry-internal/browser-utils@10.56.0':
resolution: {integrity: sha512-I8tZWAFg8SZpD8BFUpglEtSTzhZjacmcThB5/Mlq/iFiiT8mBPG4ZWDWssSfmIBKvZywJZJ83uDA0+uiJU73Tw==}
'@sentry-internal/browser-utils@10.57.0':
resolution: {integrity: sha512-tXObp954rMTSYKlbftjVXHtNl4t/6ssks3jkqyzmKb+PDPWzabGQO7sWwqVuTjT8Kx/8A3FmriS1bGmqxiJy3A==}
engines: {node: '>=18'}
'@sentry-internal/feedback@10.56.0':
resolution: {integrity: sha512-fkRR9JroESTIlErkht3OrH4DXKd/DbPozr2KLdX7boMo31hPu4cL9fuqzwOrwyDPRq9B4j+qEgIWB8JrTbgvmg==}
'@sentry-internal/feedback@10.57.0':
resolution: {integrity: sha512-ZcF4QhkqGX3iiQSXB2N0N3Awp+j5iqnDRu6PA/qyLFrWqH5ZiiAAgu59OLD9E6XAdg6iFtLYw19MAMZVK8qNOQ==}
engines: {node: '>=18'}
'@sentry-internal/replay-canvas@10.56.0':
resolution: {integrity: sha512-SDg2K0CAZT/TnhrixQGwXoi6ZsWUB+DQy3UUk0bSQm6c/5k5zFBpGOiughQN+DYsDilKREfPKmUEEnqvUjm1HQ==}
'@sentry-internal/replay-canvas@10.57.0':
resolution: {integrity: sha512-zsfa4JcfV0AEc9YhNxNabd5lSZL2Av84saAyexGAqcHs+67m9Gd0cGStOzMb/nCl7UAtmdP0aI+G7a3rcxxN/A==}
engines: {node: '>=18'}
'@sentry-internal/replay@10.56.0':
resolution: {integrity: sha512-DjF09hpy3TF7Km/kOZc73YJmBqcbPCxuZ5rtRs+KtVHu3Vq48xeW83qKUcFEZv20ur9UD99OAJ/gaEt//1Qbwg==}
'@sentry-internal/replay@10.57.0':
resolution: {integrity: sha512-Wmnx/6ABynVH1iwuoNUqJNyjIUqsqoGML7qsyivBRKb5Wo2YQtPOQlQYfxfZSvWzGpcoSVdInkRjDssUQxQEQg==}
engines: {node: '>=18'}
'@sentry-internal/server-utils@10.56.0':
resolution: {integrity: sha512-6kuZI/vAjyVKMm1cTzc2pdUmVR4Px4etMG6wnCPyFnwEaGbUKQnTynUBFpTuo/q6Js6QBQvhLNoAnO4YsOfW4w==}
'@sentry-internal/server-utils@10.57.0':
resolution: {integrity: sha512-Qu8ETmX/ITzteG7Im46b9HOxKKzeaIeqNvftaIlFURu1RUQdHbtGerS7QOmXzwnhuqNGNeiCQYkduB798IfRqA==}
engines: {node: '>=18'}
'@sentry/babel-plugin-component-annotate@5.3.0':
resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==}
engines: {node: '>= 18'}
'@sentry/browser@10.56.0':
resolution: {integrity: sha512-80X3NmsGB6tLmfzXYdjzWWdVAdL5CRukGKLcRWIcNhgGjtskOmnzaGb93egEZGI5bUTbtONJ0oyscQ3Z9yoAtQ==}
'@sentry/browser@10.57.0':
resolution: {integrity: sha512-s36AQy/CKXTfyY9Z+qUhzNomntZXgfs0rbaK7q9ffnFkqcPwzE8qQtVs58y3Suut56u+AhwSztgQtERcuZ5VIA==}
engines: {node: '>=18'}
'@sentry/bundler-plugin-core@5.3.0':
@@ -1931,12 +1931,12 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@sentry/core@10.56.0':
resolution: {integrity: sha512-L+u1dIz5SANrmST5jhIwETtt4apILgKrylv12X4hKJU0PvZl+NorjeV/ty3MwzpKQPg6b6q6qMOSLc1rLpy3iQ==}
'@sentry/core@10.57.0':
resolution: {integrity: sha512-kntItTA2kiT0YpL7encXaF6mkdZMB+y48lwj8w1wkfBpfJAC7sifdgrzLQZqmsqVNE3crg9VfufaAGA+78uFMg==}
engines: {node: '>=18'}
'@sentry/node-core@10.56.0':
resolution: {integrity: sha512-61lD2Wjtv5Lw2F3lJarcD0ORjR4GlVxrEd6w6Of/uF3DH73dD6K3n/3wXEeCIRfV/kgiCFIrCIq76nz0LVgE5g==}
'@sentry/node-core@10.57.0':
resolution: {integrity: sha512-2v2IF6MfTiu7pimWEq2rYhZsmlwyNbs3bHUsrYFPeP/Rpa6ObDuUWPdVEzJjfyK+AqqYZYxZdV0l3+B13kTEmQ==}
engines: {node: '>=18'}
peerDependencies:
'@opentelemetry/api': ^1.9.0
@@ -1959,12 +1959,12 @@ packages:
'@opentelemetry/semantic-conventions':
optional: true
'@sentry/node@10.56.0':
resolution: {integrity: sha512-qvgtXHkcR4CH3fh0VEVyw4Ysc6MMiAnm727NdTTm0yU5e53erCeo2521+yfJkqmRTGiOSgwA7B5Bs+ot9j0vFQ==}
'@sentry/node@10.57.0':
resolution: {integrity: sha512-7KEStrJ97wPf1fA5nU5ONeTTcIIlh7oT8OMffEVA1PXmlhFoXhcQZVzr4rM+zj9tfMWT01og5Ng/Grgh3dN+FA==}
engines: {node: '>=18'}
'@sentry/opentelemetry@10.56.0':
resolution: {integrity: sha512-PtMudApHMHvttjos3b7JZ2gJ+nstHAOYE3vKPYB5o0WQO95ldiaYnpLKMCRIGZWF3Dk7ynrqqnBpn8LZLt+Mrg==}
'@sentry/opentelemetry@10.57.0':
resolution: {integrity: sha512-iwRz8cEK0GOISG34aJRO8GdYOk3nfpuT6dT2GDQrxw8f7JjkJKx9LPU8MaenOFa4MhY+Z02hI6NNcrbsoI3cXg==}
engines: {node: '>=18'}
peerDependencies:
'@opentelemetry/api': ^1.9.0
@@ -1972,16 +1972,16 @@ packages:
'@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0
'@opentelemetry/semantic-conventions': ^1.39.0
'@sentry/react-router@10.56.0':
resolution: {integrity: sha512-IWbok9YoOH5e3RqCu4a6vKVBJueau8u1hOTOAeGf1NdqL9BnZuQYaoSQvDzpNXUl+gbtcbmi3IsKSeK4QCeMng==}
'@sentry/react-router@10.57.0':
resolution: {integrity: sha512-IWdlqvI46JFpYvHgYQPxNpszVFK12akqKOgudBfEPZFNNzjrEELHyAgj67QWpRCRhpm9y/1UCOVdrQzPcZ3ZSg==}
engines: {node: '>=20'}
peerDependencies:
'@react-router/node': 7.x
react: '>=18'
react-router: 7.x
'@sentry/react@10.56.0':
resolution: {integrity: sha512-HfPLyvnrydfyjRXw9Q0GMzj7w2YtEwuC9z5RrPUfarA2qpA0/J8cfGLzyFX2v0jBmA/kkj6J1uBUoSVhCTxFHg==}
'@sentry/react@10.57.0':
resolution: {integrity: sha512-6QThwQ4XWQ2rwKZEVQ9P9WKl7JlowC7S5LpAvmMdrwlfJBpLDFOsM7tycnIvbXTXf0ZOOuLFPa4L4YYbdyNGmA==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
@@ -3101,8 +3101,8 @@ packages:
peerDependencies:
acorn: ^8
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
acorn@8.17.0:
resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -3961,8 +3961,8 @@ packages:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
markdown-to-jsx@9.8.1:
resolution: {integrity: sha512-yq70dLPkBnE2LYFtGTLfRes4qyBDS+a4wDttAA/b/BzVGrbs2e0TfCeSFrMkapCg1lsxYi+42BowuBDxLP9k4Q==}
markdown-to-jsx@9.8.2:
resolution: {integrity: sha512-rWUuxKB5NsuJmSfUOuXkQ0O5qk0J/Lr3Lk6dzxKoKQI/jeHYlsVfz3zJdMLAhI46hHoXDYERWhtBOiqtWDZ4LA==}
engines: {node: '>= 18'}
peerDependencies:
react: '>= 16.0.0'
@@ -4496,8 +4496,8 @@ packages:
rematrix@0.2.2:
resolution: {integrity: sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==}
remeda@2.38.0:
resolution: {integrity: sha512-yhZjp7dd+L0NWS8gn4caKOHI6ALfbN3/2H5WNBCnFyzPYcG5vOw5b30FVpDFdQ+7Ui62QiCWKubJhG9Y5SqF5A==}
remeda@2.39.0:
resolution: {integrity: sha512-3Ki8dU1o3OVu4dwIQ2Pj+yiuP7OnEbmWAGmJ3yDRqopily5jsj8NWzPvbS89H85d6UdONKEcUnrfuHY6jN9vyw==}
engines: {node: '>=18.0.0'}
remix-auth-oauth2@3.4.1:
@@ -5182,7 +5182,7 @@ snapshots:
'@smithy/types': 4.15.0
tslib: 2.8.1
'@aws-sdk/client-s3@3.1064.0':
'@aws-sdk/client-s3@3.1065.0':
dependencies:
'@aws-crypto/sha1-browser': 5.2.0
'@aws-crypto/sha256-browser': 5.2.0
@@ -5294,9 +5294,9 @@ snapshots:
'@smithy/types': 4.15.0
tslib: 2.8.1
'@aws-sdk/lib-storage@3.1064.0(@aws-sdk/client-s3@3.1064.0)':
'@aws-sdk/lib-storage@3.1065.0(@aws-sdk/client-s3@3.1065.0)':
dependencies:
'@aws-sdk/client-s3': 3.1064.0
'@aws-sdk/client-s3': 3.1065.0
'@smithy/core': 3.25.0
'@smithy/types': 4.15.0
buffer: 5.6.0
@@ -5814,7 +5814,7 @@ snapshots:
'@opentelemetry/api@1.9.1': {}
'@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)':
'@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/semantic-conventions': 1.41.1
@@ -5828,17 +5828,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)':
'@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.41.1
'@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)':
'@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.41.1
'@opentelemetry/semantic-conventions@1.41.1': {}
@@ -6600,37 +6600,37 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.62.0':
optional: true
'@sentry-internal/browser-utils@10.56.0':
'@sentry-internal/browser-utils@10.57.0':
dependencies:
'@sentry/core': 10.56.0
'@sentry/core': 10.57.0
'@sentry-internal/feedback@10.56.0':
'@sentry-internal/feedback@10.57.0':
dependencies:
'@sentry/core': 10.56.0
'@sentry/core': 10.57.0
'@sentry-internal/replay-canvas@10.56.0':
'@sentry-internal/replay-canvas@10.57.0':
dependencies:
'@sentry-internal/replay': 10.56.0
'@sentry/core': 10.56.0
'@sentry-internal/replay': 10.57.0
'@sentry/core': 10.57.0
'@sentry-internal/replay@10.56.0':
'@sentry-internal/replay@10.57.0':
dependencies:
'@sentry-internal/browser-utils': 10.56.0
'@sentry/core': 10.56.0
'@sentry-internal/browser-utils': 10.57.0
'@sentry/core': 10.57.0
'@sentry-internal/server-utils@10.56.0':
'@sentry-internal/server-utils@10.57.0':
dependencies:
'@sentry/core': 10.56.0
'@sentry/core': 10.57.0
'@sentry/babel-plugin-component-annotate@5.3.0': {}
'@sentry/browser@10.56.0':
'@sentry/browser@10.57.0':
dependencies:
'@sentry-internal/browser-utils': 10.56.0
'@sentry-internal/feedback': 10.56.0
'@sentry-internal/replay': 10.56.0
'@sentry-internal/replay-canvas': 10.56.0
'@sentry/core': 10.56.0
'@sentry-internal/browser-utils': 10.57.0
'@sentry-internal/feedback': 10.57.0
'@sentry-internal/replay': 10.57.0
'@sentry-internal/replay-canvas': 10.57.0
'@sentry/core': 10.57.0
'@sentry/bundler-plugin-core@5.3.0':
dependencies:
@@ -6689,56 +6689,56 @@ snapshots:
- encoding
- supports-color
'@sentry/core@10.56.0': {}
'@sentry/core@10.57.0': {}
'@sentry/node-core@10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)':
'@sentry/node-core@10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)':
dependencies:
'@sentry/core': 10.56.0
'@sentry/opentelemetry': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)
'@sentry/core': 10.57.0
'@sentry/opentelemetry': 10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)
import-in-the-middle: 3.0.2
optionalDependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.41.1
'@sentry/node@10.56.0':
'@sentry/node@10.57.0':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.41.1
'@sentry-internal/server-utils': 10.56.0
'@sentry/core': 10.56.0
'@sentry/node-core': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)
'@sentry/opentelemetry': 10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)
'@sentry-internal/server-utils': 10.57.0
'@sentry/core': 10.57.0
'@sentry/node-core': 10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)
'@sentry/opentelemetry': 10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)
import-in-the-middle: 3.0.2
transitivePeerDependencies:
- '@opentelemetry/exporter-trace-otlp-http'
- supports-color
'@sentry/opentelemetry@10.56.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)':
'@sentry/opentelemetry@10.57.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.41.1
'@sentry/core': 10.56.0
'@sentry/core': 10.57.0
'@sentry/react-router@10.56.0(@react-router/node@7.17.0(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3))(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rollup@4.62.0)':
'@sentry/react-router@10.57.0(@react-router/node@7.17.0(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3))(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rollup@4.62.0)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1)
'@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.41.1
'@react-router/node': 7.17.0(react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(typescript@6.0.3)
'@sentry/browser': 10.56.0
'@sentry/browser': 10.57.0
'@sentry/cli': 2.58.6
'@sentry/core': 10.56.0
'@sentry/node': 10.56.0
'@sentry/react': 10.56.0(react@19.2.7)
'@sentry/core': 10.57.0
'@sentry/node': 10.57.0
'@sentry/react': 10.57.0(react@19.2.7)
'@sentry/vite-plugin': 5.3.0(rollup@4.62.0)
glob: 13.0.6
react: 19.2.7
@@ -6749,10 +6749,10 @@ snapshots:
- rollup
- supports-color
'@sentry/react@10.56.0(react@19.2.7)':
'@sentry/react@10.57.0(react@19.2.7)':
dependencies:
'@sentry/browser': 10.56.0
'@sentry/core': 10.56.0
'@sentry/browser': 10.57.0
'@sentry/core': 10.57.0
react: 19.2.7
'@sentry/rollup-plugin@5.3.0(rollup@4.62.0)':
@@ -8094,11 +8094,11 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
acorn-import-attributes@1.9.5(acorn@8.16.0):
acorn-import-attributes@1.9.5(acorn@8.17.0):
dependencies:
acorn: 8.16.0
acorn: 8.17.0
acorn@8.16.0: {}
acorn@8.17.0: {}
agent-base@6.0.2:
dependencies:
@@ -8748,8 +8748,8 @@ snapshots:
import-in-the-middle@3.0.2:
dependencies:
acorn: 8.16.0
acorn-import-attributes: 1.9.5(acorn@8.16.0)
acorn: 8.17.0
acorn-import-attributes: 1.9.5(acorn@8.17.0)
cjs-module-lexer: 2.2.0
module-details-from-path: 1.0.4
@@ -8934,7 +8934,7 @@ snapshots:
punycode.js: 2.3.1
uc.micro: 2.1.0
markdown-to-jsx@9.8.1(react@19.2.7):
markdown-to-jsx@9.8.2(react@19.2.7):
optionalDependencies:
react: 19.2.7
@@ -9496,7 +9496,7 @@ snapshots:
rematrix@0.2.2: {}
remeda@2.38.0: {}
remeda@2.39.0: {}
remix-auth-oauth2@3.4.1(remix-auth@4.2.0):
dependencies: