Migrate /u/:identifier/edit to SendouForm, fix bad IGN (#2849)
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle 2026-03-03 07:12:04 +02:00 committed by GitHub
parent c90b0b4ee3
commit c1cc82c807
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 1062 additions and 1243 deletions

View File

@ -28,12 +28,16 @@ type ContrastArray = {
export function CustomizedColorsInput({
initialColors,
value: controlledValue,
onChange,
}: {
initialColors?: Record<string, string> | null;
value?: Record<string, string> | null;
onChange?: (value: Record<string, string> | null) => void;
}) {
const { t } = useTranslation();
const [colors, setColors] = React.useState<CustomColorsRecord>(
initialColors ?? {},
controlledValue ?? initialColors ?? {},
);
const [defaultColors, setDefaultColors] = React.useState<
@ -41,6 +45,15 @@ export function CustomizedColorsInput({
>([]);
const [contrasts, setContrast] = React.useState<ContrastArray>([]);
const updateColors = (newColors: CustomColorsRecord) => {
setColors(newColors);
if (onChange) {
const filtered = colorsWithDefaultsFilteredOut(newColors, defaultColors);
const hasValues = Object.keys(filtered).length > 0;
onChange(hasValues ? (filtered as Record<string, string>) : null);
}
};
useDebounce(
() => {
for (const color in colors) {
@ -79,13 +92,15 @@ export function CustomizedColorsInput({
</summary>
<div>
<Label>{t("custom.colors.title")}</Label>
<input
type="hidden"
name="css"
value={JSON.stringify(
colorsWithDefaultsFilteredOut(colors, defaultColors),
)}
/>
{!onChange ? (
<input
type="hidden"
name="css"
value={JSON.stringify(
colorsWithDefaultsFilteredOut(colors, defaultColors),
)}
/>
) : null}
<div className="colors__container colors__grid">
{CUSTOM_CSS_VAR_COLORS.filter(
(cssVar) => cssVar !== "bg-lightest",
@ -102,7 +117,7 @@ export function CustomizedColorsInput({
if (cssVar === "bg-lighter") {
extras["bg-lightest"] = `${e.target.value}80`;
}
setColors({
updateColors({
...colors,
...extras,
[cssVar]: e.target.value,
@ -122,7 +137,7 @@ export function CustomizedColorsInput({
(color) => color["bg-lightest"],
)?.["bg-lightest"];
}
setColors({
updateColors({
...newColors,
[cssVar]: defaultColors.find((color) => color[cssVar])?.[
cssVar

View File

@ -1,79 +1,85 @@
import { type ActionFunction, redirect } from "react-router";
import { requireUser } from "~/features/auth/core/user.server";
import { BADGE } from "~/features/badges/badges-constants";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { clearTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { safeParseRequestFormData } from "~/utils/remix.server";
import { errorIsSqliteUniqueConstraintFailure } from "~/utils/sql";
import { parseFormData } from "~/form/parse.server";
import { userPage } from "~/utils/urls";
import { userEditActionSchema } from "../user-page-schemas";
import { userEditProfileSchemaServer } from "../user-page-schemas.server";
export const action: ActionFunction = async ({ request }) => {
const parsedInput = await safeParseRequestFormData({
const user = requireUser();
const result = await parseFormData({
request,
schema: userEditActionSchema,
schema: userEditProfileSchemaServer,
});
if (!parsedInput.success) {
return {
errors: parsedInput.errors,
};
if (!result.success) {
return { fieldErrors: result.fieldErrors };
}
const {
inGameNameText,
inGameNameDiscriminator,
newProfileEnabled,
...data
} = parsedInput.data;
const data = result.data;
const user = requireUser();
const inGameName =
inGameNameText && inGameNameDiscriminator
? `${inGameNameText}#${inGameNameDiscriminator}`
const [subjectPronoun, objectPronoun] = data.pronouns ?? [null, null];
const pronouns =
subjectPronoun && objectPronoun
? JSON.stringify({ subject: subjectPronoun, object: objectPronoun })
: null;
try {
const pronouns =
data.subjectPronoun && data.objectPronoun
? JSON.stringify({
subject: data.subjectPronoun,
object: data.objectPronoun,
})
: null;
const [motionSens, stickSens] = data.sensitivity ?? [null, null];
const editedUser = await UserRepository.updateProfile({
...data,
pronouns,
inGameName,
userId: user.id,
});
const weapons = data.weapons.map((w) => ({
weaponSplId: w.id,
isFavorite: w.isFavorite ? (1 as const) : (0 as const),
}));
await UserRepository.updatePreferences(user.id, {
newProfileEnabled: Boolean(newProfileEnabled),
});
const css = data.css ? JSON.stringify(data.css) : null;
// TODO: to transaction
if (inGameName) {
const tournamentIdsAffected =
await TournamentTeamRepository.updateMemberInGameNameForNonStarted({
inGameName,
userId: user.id,
});
const isSupporter = user.roles?.includes("SUPPORTER");
const isArtist = user.roles?.includes("ARTIST");
for (const tournamentId of tournamentIdsAffected) {
clearTournamentDataCache(tournamentId);
}
const maxBadgeCount = isSupporter
? BADGE.SMALL_BADGES_PER_DISPLAY_PAGE + 1
: 1;
const limitedBadgeIds = data.favoriteBadgeIds.slice(0, maxBadgeCount);
const editedUser = await UserRepository.updateProfile({
userId: user.id,
country: data.country,
bio: data.bio,
customUrl: data.customUrl,
customName: data.customName,
motionSens: motionSens !== null ? Number(motionSens) : null,
stickSens: stickSens !== null ? Number(stickSens) : null,
pronouns,
inGameName: data.inGameName,
css: isSupporter ? css : null,
battlefy: data.battlefy,
weapons,
favoriteBadgeIds: limitedBadgeIds.length > 0 ? limitedBadgeIds : null,
showDiscordUniqueName: data.showDiscordUniqueName ? 1 : 0,
commissionsOpen: isArtist && data.commissionsOpen ? 1 : 0,
commissionText: isArtist ? data.commissionText : null,
});
await UserRepository.updatePreferences(user.id, {
newProfileEnabled: isSupporter ? data.newProfileEnabled : false,
});
// TODO: to transaction
if (data.inGameName) {
const tournamentIdsAffected =
await TournamentTeamRepository.updateMemberInGameNameForNonStarted({
inGameName: data.inGameName,
userId: user.id,
});
for (const tournamentId of tournamentIdsAffected) {
clearTournamentDataCache(tournamentId);
}
throw redirect(userPage(editedUser));
} catch (e) {
if (!errorIsSqliteUniqueConstraintFailure(e)) {
throw e;
}
return {
errors: ["forms.errors.invalidCustomUrl.duplicate"],
};
}
throw redirect(userPage(editedUser));
};

View File

@ -1,33 +1,30 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { dbInsertUsers, dbReset, wrappedAction } from "~/utils/Test";
import type { userEditActionSchema } from "../user-page-schemas";
import type { userEditProfileBaseSchema } from "../user-page-schemas";
import { action as editUserProfileAction } from "./u.$identifier.edit";
const action = wrappedAction<typeof userEditActionSchema>({
const action = wrappedAction<typeof userEditProfileBaseSchema>({
action: editUserProfileAction,
isJsonSubmission: true,
});
const DEFAULT_FIELDS = {
battlefy: null,
bio: null,
commissionsOpen: 1,
commissionsOpen: false,
commissionText: null,
country: "FI",
customName: null,
customUrl: null,
favoriteBadgeIds: null,
inGameNameDiscriminator: null,
inGameNameText: null,
motionSens: null,
showDiscordUniqueName: 1,
newProfileEnabled: 0,
stickSens: null,
subjectPronoun: null,
objectPronoun: null,
weapons: JSON.stringify([
{ weaponSplId: 1 as MainWeaponId, isFavorite: 0 },
]) as any,
favoriteBadgeIds: [],
inGameName: null,
sensitivity: [null, null] as [null, null],
pronouns: [null, null] as [null, null],
weapons: [{ id: 1 as MainWeaponId, isFavorite: false }],
showDiscordUniqueName: true,
newProfileEnabled: false,
css: null,
};
describe("user page editing", () => {
@ -41,8 +38,8 @@ describe("user page editing", () => {
it("adds valid custom css vars", async () => {
const response = await action(
{
css: JSON.stringify({ bg: "#fff" }),
...DEFAULT_FIELDS,
css: { bg: "#fff" },
},
{ user: "regular", params: { identifier: "2" } },
);
@ -53,28 +50,28 @@ describe("user page editing", () => {
it("prevents adding custom css var of unknown property", async () => {
const res = await action(
{
css: JSON.stringify({
"backdrop-filter": "#fff",
}),
...DEFAULT_FIELDS,
css: {
"backdrop-filter": "#fff",
} as any,
},
{ user: "regular", params: { identifier: "2" } },
);
expect(res.errors[0]).toBe("Invalid custom CSS var object");
expect(res.fieldErrors.css).toBeDefined();
});
it("prevents adding custom css var of unknown value", async () => {
const res = await action(
{
css: JSON.stringify({
bg: "url(https://sendou.ink/u?q=1&_data=features%2Fuser-search%2Froutes%2Fu)",
}),
...DEFAULT_FIELDS,
css: {
bg: "url(https://sendou.ink/u?q=1&_data=features%2Fuser-search%2Froutes%2Fu)",
},
},
{ user: "regular", params: { identifier: "2" } },
);
expect(res.errors[0]).toBe("Invalid custom CSS var object");
expect(res.fieldErrors.css).toBeDefined();
});
});

View File

@ -1,37 +1,26 @@
import clsx from "clsx";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { Form, Link, useLoaderData, useMatches } from "react-router";
import { Link, useLoaderData, useMatches } from "react-router";
import { CustomizedColorsInput } from "~/components/CustomizedColorsInput";
import { SendouButton } from "~/components/elements/Button";
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
import { SendouSwitch } from "~/components/elements/Switch";
import { FormErrors } from "~/components/FormErrors";
import { FormMessage } from "~/components/FormMessage";
import { WeaponImage } from "~/components/Image";
import { Input } from "~/components/Input";
import { StarIcon } from "~/components/icons/Star";
import { StarFilledIcon } from "~/components/icons/StarFilled";
import { TrashIcon } from "~/components/icons/Trash";
import { Label } from "~/components/Label";
import { SubmitButton } from "~/components/SubmitButton";
import { WeaponSelect } from "~/components/WeaponSelect";
import { OBJECT_PRONOUNS, SUBJECT_PRONOUNS, type Tables } from "~/db/tables";
import { BADGE } from "~/features/badges/badges-constants";
import { BadgesSelector } from "~/features/badges/components/BadgesSelector";
import type { CustomFieldRenderProps } from "~/form/FormField";
import { SendouForm } from "~/form/SendouForm";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useHasRole } from "~/modules/permissions/hooks";
import { countryCodeToTranslatedName } from "~/utils/i18n";
import invariant from "~/utils/invariant";
import { rawSensToString } from "~/utils/strings";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { FAQ_PAGE } from "~/utils/urls";
import { action } from "../actions/u.$identifier.edit.server";
import { loader } from "../loaders/u.$identifier.edit.server";
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
import { COUNTRY_CODES, USER } from "../user-page-constants";
import { COUNTRY_CODES } from "../user-page-constants";
import { userEditProfileBaseSchema } from "../user-page-schemas";
export { loader, action };
import styles from "~/styles/u.$identifier.module.css";
export const handle: SendouRouteHandle = {
i18n: ["user"],
};
export default function UserEditPage() {
const { t } = useTranslation(["common", "user"]);
@ -43,545 +32,141 @@ export default function UserEditPage() {
const isSupporter = useHasRole("SUPPORTER");
const isArtist = useHasRole("ARTIST");
const countryOptions = useCountryOptions();
const badgeOptions = data.user.badges.map((badge) => ({
id: badge.id,
displayName: badge.displayName,
code: badge.code,
hue: badge.hue,
}));
const defaultValues = {
css: layoutData.css ?? null,
customName: data.user.customName ?? "",
customUrl: layoutData.user.customUrl ?? "",
inGameName: data.user.inGameName ?? "",
sensitivity: sensDefaultValue(data.user.motionSens, data.user.stickSens),
pronouns: pronounsDefaultValue(data.user.pronouns),
battlefy: data.user.battlefy ?? "",
country: data.user.country ?? null,
favoriteBadgeIds: data.favoriteBadgeIds ?? [],
weapons: data.user.weapons.map((w) => ({
id: w.weaponSplId,
isFavorite: Boolean(w.isFavorite),
})),
bio: data.user.bio ?? "",
showDiscordUniqueName: Boolean(data.user.showDiscordUniqueName),
commissionsOpen: Boolean(layoutData.user.commissionsOpen),
commissionText: layoutData.user.commissionText ?? "",
newProfileEnabled: isSupporter && data.newProfileEnabled,
};
return (
<div className="half-width">
<Form className={styles.container} method="post">
{isSupporter ? (
<CustomizedColorsInput initialColors={layoutData.css} />
) : null}
<CustomNameInput />
<CustomUrlInput parentRouteData={layoutData} />
<InGameNameInputs />
<SensSelects />
<PronounsSelect />
<BattlefyInput />
<CountrySelect />
<FavBadgeSelect />
<WeaponPoolSelect />
<BioTextarea initialValue={data.user.bio} />
{data.discordUniqueName ? (
<ShowUniqueDiscordNameToggle />
) : (
<input type="hidden" name="showDiscordUniqueName" value="on" />
)}
{isArtist ? (
<SendouForm
schema={userEditProfileBaseSchema}
defaultValues={defaultValues}
submitButtonText={t("common:actions.save")}
>
{({ FormField }) => (
<>
<CommissionsOpenToggle parentRouteData={layoutData} />
<CommissionTextArea initialValue={layoutData.user.commissionText} />
</>
) : (
<>
<input type="hidden" name="commissionsOpen" value="off" />
<input type="hidden" name="commissionText" value="" />
{isSupporter ? (
<FormField name="css">
{(props: CustomFieldRenderProps) => (
<CssCustomField {...props} initialColors={layoutData.css} />
)}
</FormField>
) : null}
<FormField name="customName" />
<FormField name="customUrl" />
<FormField name="inGameName" />
<FormField name="sensitivity" />
<FormField name="pronouns" />
<FormField name="battlefy" />
<FormField name="country" options={countryOptions} />
{data.user.badges.length >= 2 ? (
<FormField
name="favoriteBadgeIds"
options={badgeOptions}
maxCount={
isSupporter ? BADGE.SMALL_BADGES_PER_DISPLAY_PAGE + 1 : 1
}
/>
) : null}
<FormField name="weapons" />
<FormField name="bio" />
{data.discordUniqueName ? (
<FormField name="showDiscordUniqueName" />
) : null}
{isArtist ? (
<>
<FormField name="commissionsOpen" />
<FormField name="commissionText" />
</>
) : null}
<FormField name="newProfileEnabled" disabled={!isSupporter} />
<FormMessage type="info">
<Trans i18nKey={"user:discordExplanation"} t={t}>
Username, profile picture, YouTube, Bluesky and Twitch accounts
come from your Discord account. See{" "}
<Link to={FAQ_PAGE}>FAQ</Link> for more information.
</Trans>
</FormMessage>
</>
)}
<NewProfileToggle />
<FormMessage type="info">
<Trans i18nKey={"user:discordExplanation"} t={t}>
Username, profile picture, YouTube, Bluesky and Twitch accounts come
from your Discord account. See <Link to={FAQ_PAGE}>FAQ</Link> for
more information.
</Trans>
</FormMessage>
<SubmitButton>{t("common:actions.save")}</SubmitButton>
<FormErrors namespace="user" />
</Form>
</SendouForm>
</div>
);
}
function CustomUrlInput({
parentRouteData,
}: {
parentRouteData: UserPageLoaderData;
}) {
const { t } = useTranslation(["user"]);
return (
<div className="w-full">
<Label htmlFor="customUrl">{t("user:customUrl")}</Label>
<Input
name="customUrl"
id="customUrl"
leftAddon="https://sendou.ink/u/"
maxLength={USER.CUSTOM_URL_MAX_LENGTH}
defaultValue={parentRouteData.user.customUrl ?? undefined}
/>
<FormMessage type="info">{t("user:forms.info.customUrl")}</FormMessage>
</div>
);
}
function CustomNameInput() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
return (
<div className="w-full">
<Label htmlFor="customName">{t("user:customName")}</Label>
<Input
name="customName"
id="customName"
maxLength={USER.CUSTOM_NAME_MAX_LENGTH}
defaultValue={data.user.customName ?? undefined}
/>
<FormMessage type="info">
{t("user:forms.customName.info", {
discordName: data.user.discordName,
})}
</FormMessage>
</div>
);
}
function InGameNameInputs() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
const inGameNameParts = data.user.inGameName?.split("#");
return (
<div className="stack items-start">
<Label>{t("user:ign")}</Label>
<div className="stack horizontal sm items-center">
<Input
className={styles.inGameNameText}
name="inGameNameText"
aria-label="In game name"
maxLength={USER.IN_GAME_NAME_TEXT_MAX_LENGTH}
defaultValue={inGameNameParts?.[0]}
/>
<div className={styles.inGameNameHashtag}>#</div>
<Input
className={styles.inGameNameDiscriminator}
name="inGameNameDiscriminator"
aria-label="In game name discriminator"
maxLength={USER.IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH}
pattern="[0-9a-z]{4,5}"
defaultValue={inGameNameParts?.[1]}
/>
</div>
</div>
);
}
const SENS_OPTIONS = [
-50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35,
40, 45, 50,
];
function SensSelects() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
return (
<div className="stack horizontal md">
<div>
<Label htmlFor="motionSens">{t("user:motionSens")}</Label>
<select
id="motionSens"
name="motionSens"
defaultValue={data.user.motionSens ?? undefined}
className={styles.sensSelect}
>
<option value="">{"-"}</option>
{SENS_OPTIONS.map((sens) => (
<option key={sens} value={sens}>
{rawSensToString(Number(sens))}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="stickSens">{t("user:stickSens")}</Label>
<select
id="stickSens"
name="stickSens"
defaultValue={data.user.stickSens ?? undefined}
className={styles.sensSelect}
>
<option value="">{"-"}</option>
{SENS_OPTIONS.map((sens) => (
<option key={sens} value={sens}>
{rawSensToString(Number(sens))}
</option>
))}
</select>
</div>
</div>
);
}
function PronounsSelect() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
return (
<div>
<div className="stack horizontal md">
<div>
<Label htmlFor="subjectPronoun">{t("user:pronoun")}</Label>
<select
id="subjectPronoun"
name="subjectPronoun"
defaultValue={data.user.pronouns?.subject ?? undefined}
className={styles.sensSelect}
>
<option value="">{"-"}</option>
{SUBJECT_PRONOUNS.map((pronoun) => (
<option key={pronoun} value={pronoun}>
{pronoun}
</option>
))}
</select>
<span className={styles.seperator}>/</span>
</div>
<div>
<Label htmlFor="objectPronoun">{t("user:pronoun")}</Label>
<select
id="objectPronoun"
name="objectPronoun"
defaultValue={data.user.pronouns?.object ?? undefined}
className={styles.sensSelect}
>
<option value="">{"-"}</option>
{OBJECT_PRONOUNS.map((pronoun) => (
<option key={pronoun} value={pronoun}>
{pronoun}
</option>
))}
</select>
</div>
</div>
<FormMessage type="info">{t("user:pronounsInfo")}</FormMessage>
</div>
);
}
function CountrySelect() {
const { t, i18n } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
function useCountryOptions() {
const { i18n } = useTranslation();
const isMounted = useIsMounted();
const [value, setValue] = React.useState(data.user.country ?? null);
// TODO: if react-aria-components start supporting "suppressHydrationWarning" it would likely be a better solution here
const items = COUNTRY_CODES.map((countryCode) => ({
name: isMounted
return COUNTRY_CODES.map((countryCode) => ({
value: countryCode,
label: isMounted
? countryCodeToTranslatedName({
countryCode,
language: i18n.language,
})
: countryCode,
id: countryCode,
key: countryCode,
})).sort((a, b) =>
a.name.localeCompare(b.name, i18n.language, { sensitivity: "base" }),
);
return (
<>
{/* TODO: this is a workaround for clearable not working with uncontrolled values, in future the component should handle this one way or another */}
<input type="hidden" name="country" value={value ?? ""} />
<SendouSelect
items={items}
label={t("user:country")}
search={{
placeholder: t("user:forms.country.search.placeholder"),
}}
selectedKey={value}
onSelectionChange={(value) => setValue(value as string | null)}
clearable
>
{({ key, ...item }) => (
<SendouSelectItem key={key} {...item}>
{item.name}
</SendouSelectItem>
)}
</SendouSelect>
</>
a.label.localeCompare(b.label, i18n.language, { sensitivity: "base" }),
);
}
function BattlefyInput() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
return (
<div className="w-full">
<Label htmlFor="customName">{t("user:battlefy")}</Label>
<Input
name="battlefy"
id="battlefy"
maxLength={USER.BATTLEFY_MAX_LENGTH}
defaultValue={data.user.battlefy ?? undefined}
leftAddon="https://battlefy.com/users/"
/>
<FormMessage type="info">{t("user:forms.info.battlefy")}</FormMessage>
</div>
);
}
function WeaponPoolSelect() {
const data = useLoaderData<typeof loader>();
const [weapons, setWeapons] = React.useState(data.user.weapons);
const { t } = useTranslation(["user"]);
const latestWeapon = weapons[weapons.length - 1];
return (
<div className={clsx("stack md", styles.weaponPool)}>
<input type="hidden" name="weapons" value={JSON.stringify(weapons)} />
{weapons.length < USER.WEAPON_POOL_MAX_SIZE ? (
<WeaponSelect
label={t("user:weaponPool")}
onChange={(weaponSplId) => {
setWeapons([
...weapons,
{
weaponSplId,
isFavorite: 0,
},
]);
}}
disabledWeaponIds={weapons.map((w) => w.weaponSplId)}
// empty on selection
key={latestWeapon?.weaponSplId ?? "empty"}
/>
) : (
<span className="text-xs text-warning">
{t("user:forms.errors.maxWeapons")}
</span>
)}
<div className="stack horizontal sm justify-center">
{weapons.map((weapon) => {
return (
<div key={weapon.weaponSplId} className="stack xs">
<div className="u__weapon">
<WeaponImage
weaponSplId={weapon.weaponSplId}
variant={weapon.isFavorite ? "badge-5-star" : "badge"}
width={38}
height={38}
/>
</div>
<div className="stack sm horizontal items-center justify-center">
<SendouButton
icon={weapon.isFavorite ? <StarFilledIcon /> : <StarIcon />}
variant="minimal"
aria-label="Favorite weapon"
onPress={() =>
setWeapons(
weapons.map((w) =>
w.weaponSplId === weapon.weaponSplId
? {
...weapon,
isFavorite: weapon.isFavorite === 1 ? 0 : 1,
}
: w,
),
)
}
/>
<SendouButton
icon={<TrashIcon />}
variant="minimal-destructive"
aria-label="Delete weapon"
onPress={() =>
setWeapons(
weapons.filter(
(w) => w.weaponSplId !== weapon.weaponSplId,
),
)
}
data-testid={`delete-weapon-${weapon.weaponSplId}`}
size="small"
/>
</div>
</div>
);
})}
</div>
</div>
);
}
function BioTextarea({
initialValue,
}: {
initialValue: Tables["User"]["bio"];
function CssCustomField({
value,
onChange,
initialColors,
}: CustomFieldRenderProps & {
initialColors?: Record<string, string> | null;
}) {
const { t } = useTranslation("user");
const [value, setValue] = React.useState(initialValue ?? "");
return (
<div className={styles.bioContainer}>
<Label
htmlFor="bio"
valueLimits={{ current: value.length, max: USER.BIO_MAX_LENGTH }}
>
{t("bio")}
</Label>
<textarea
id="bio"
name="bio"
value={value}
onChange={(e) => setValue(e.target.value)}
maxLength={USER.BIO_MAX_LENGTH}
/>
</div>
<CustomizedColorsInput
initialColors={initialColors}
value={value as Record<string, string> | null}
onChange={onChange as (value: Record<string, string> | null) => void}
/>
);
}
function FavBadgeSelect() {
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["user"]);
const [value, setValue] = React.useState(data.favoriteBadgeIds ?? []);
const isSupporter = useHasRole("SUPPORTER");
// doesn't make sense to select favorite badge
// if user has no badges or only has 1 badge
if (data.user.badges.length < 2) return null;
const onChange = (newBadges: number[]) => {
if (isSupporter) {
setValue(newBadges);
} else {
// non-supporters can only set which badge is the big one
setValue(newBadges.length > 0 ? [newBadges[0]] : []);
}
};
return (
<div>
<input
type="hidden"
name="favoriteBadgeIds"
value={JSON.stringify(value)}
/>
<label htmlFor="favoriteBadgeIds">{t("user:favoriteBadges")}</label>
<BadgesSelector
options={data.user.badges}
selectedBadges={value}
onChange={onChange}
maxCount={BADGE.SMALL_BADGES_PER_DISPLAY_PAGE + 1}
showSelect={isSupporter || value.length === 0}
>
{!isSupporter ? (
<div className="text-sm text-lighter font-semi-bold text-center">
{t("user:forms.favoriteBadges.nonSupporter")}
</div>
) : null}
</BadgesSelector>
</div>
);
function pronounsDefaultValue(
pronouns: { subject: string; object: string } | null,
): [string | null, string | null] {
if (!pronouns) return [null, null];
return [pronouns.subject, pronouns.object];
}
function NewProfileToggle() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
const isSupporter = useHasRole("SUPPORTER");
const [checked, setChecked] = React.useState(
isSupporter && data.newProfileEnabled,
);
return (
<div>
<label htmlFor="newProfileEnabled">
{t("user:forms.newProfileEnabled")}
</label>
<SendouSwitch
isSelected={checked}
onChange={setChecked}
name="newProfileEnabled"
isDisabled={!isSupporter}
/>
<FormMessage type="info">
{t("user:forms.newProfileEnabled.info")}
</FormMessage>
</div>
);
}
function ShowUniqueDiscordNameToggle() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
const [checked, setChecked] = React.useState(
Boolean(data.user.showDiscordUniqueName),
);
return (
<div>
<label htmlFor="showDiscordUniqueName">
{t("user:forms.showDiscordUniqueName")}
</label>
<SendouSwitch
isSelected={checked}
onChange={setChecked}
name="showDiscordUniqueName"
/>
<FormMessage type="info">
{t("user:forms.showDiscordUniqueName.info", {
discordUniqueName: data.discordUniqueName,
})}
</FormMessage>
</div>
);
}
function CommissionsOpenToggle({
parentRouteData,
}: {
parentRouteData: UserPageLoaderData;
}) {
const { t } = useTranslation(["user"]);
const [checked, setChecked] = React.useState(
Boolean(parentRouteData.user.commissionsOpen),
);
return (
<div>
<label htmlFor="commissionsOpen">{t("user:forms.commissionsOpen")}</label>
<SendouSwitch
isSelected={checked}
onChange={setChecked}
name="commissionsOpen"
/>
<FormMessage type="info">
{t("user:forms.commissionsOpen.info")}
</FormMessage>
</div>
);
}
function CommissionTextArea({
initialValue,
}: {
initialValue: Tables["User"]["commissionText"];
}) {
const { t } = useTranslation(["user"]);
const [value, setValue] = React.useState(initialValue ?? "");
return (
<div className={styles.bioContainer}>
<Label
htmlFor="commissionText"
valueLimits={{
current: value.length,
max: USER.COMMISSION_TEXT_MAX_LENGTH,
}}
>
{t("user:forms.commissionText")}
</Label>
<textarea
id="commissionText"
name="commissionText"
value={value}
onChange={(e) => setValue(e.target.value)}
maxLength={USER.COMMISSION_TEXT_MAX_LENGTH}
/>
<FormMessage type="info">
{t("user:forms.commissionText.info")}
</FormMessage>
</div>
);
function sensDefaultValue(
motionSens: number | null,
stickSens: number | null,
): [string | null, string | null] {
if (motionSens === null && stickSens === null) return [null, null];
return [
motionSens !== null ? String(motionSens) : null,
stickSens !== null ? String(stickSens) : null,
];
}

View File

@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { IN_GAME_NAME_REGEXP } from "./user-page-constants";
describe("IN_GAME_NAME_REGEXP", () => {
it("should pass valid in-game names", () => {
const validNames = [
"Sendou#12345",
"The Player#12345",
" a#1234",
"A#1234",
"Player#abcd",
"名前テスト1234#ab12c",
];
for (const name of validNames) {
expect(IN_GAME_NAME_REGEXP.test(name), `expected "${name}" to pass`).toBe(
true,
);
}
});
it("should not pass invalid in-game names", () => {
const invalidNames = [
"#1234",
"Sendou1234",
"Sendou#123",
"Sendou# 1234",
"Sendou#123456",
"Sendou#ABCD",
"12345678901#1234",
];
for (const name of invalidNames) {
expect(IN_GAME_NAME_REGEXP.test(name), `expected "${name}" to fail`).toBe(
false,
);
}
});
});

View File

@ -18,6 +18,8 @@ export const USER = {
GAME_BADGES_SMALL_MAX: 4,
};
export const IN_GAME_NAME_REGEXP = /^.{1,10}#[0-9a-z]{4,5}$/;
export const MATCHES_PER_SEASONS_PAGE = 8;
export const RESULTS_PER_PAGE = 25;
export const DEFAULT_BUILD_SORT = ["WEAPON_POOL", "UPDATED_AT"] as const;

View File

@ -1,6 +1,29 @@
import { z } from "zod";
import { requireUser } from "~/features/auth/core/user.server";
import * as BuildRepository from "~/features/builds/BuildRepository.server";
import { gearAllOrNoneRefine, newBuildBaseSchema } from "./user-page-schemas";
import * as UserRepository from "./UserRepository.server";
import {
gearAllOrNoneRefine,
newBuildBaseSchema,
userEditProfileBaseSchema,
} from "./user-page-schemas";
export const userEditProfileSchemaServer =
userEditProfileBaseSchema.superRefine(async (data, ctx) => {
if (!data.customUrl) return;
const existingUser = await UserRepository.findByCustomUrl(data.customUrl);
if (!existingUser) return;
const currentUser = requireUser();
if (existingUser.id === currentUser.id) return;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "forms:errors.profileCustomUrlDuplicate",
path: ["customUrl"],
});
});
export const newBuildSchemaServer = newBuildBaseSchema
.refine(gearAllOrNoneRefine.fn, gearAllOrNoneRefine.opts)

View File

@ -3,12 +3,16 @@ import { OBJECT_PRONOUNS, SUBJECT_PRONOUNS } from "~/db/tables";
import { BADGE } from "~/features/badges/badges-constants";
import * as Seasons from "~/features/mmr/core/Seasons";
import {
badges,
checkboxGroup,
customField,
dualSelectOptional,
idConstantOptional,
selectDynamicOptional,
stringConstant,
textAreaOptional,
textAreaRequired,
textFieldOptional,
textFieldRequired,
toggle,
weaponPool,
@ -18,32 +22,23 @@ import {
headGearIds,
shoesGearIds,
} from "~/modules/in-game-lists/gear-ids";
import { rawSensToString } from "~/utils/strings";
import { isCustomUrl } from "~/utils/urls";
import {
_action,
actualNumber,
checkboxValueToDbBoolean,
clothesMainSlotAbility,
customCssVarObject,
dbBoolean,
emptyArrayToNull,
falsyToNull,
headMainSlotAbility,
id,
nullLiteraltoNull,
processMany,
safeJSONParse,
safeNullableStringSchema,
shoesMainSlotAbility,
stackableAbility,
undefinedToNull,
weaponSplId,
} from "~/utils/zod";
import { allWidgetsFlat, findWidgetById } from "./core/widgets/portfolio";
import {
COUNTRY_CODES,
CUSTOM_CSS_VAR_COLORS,
HIGHLIGHT_CHECKBOX_NAME,
HIGHLIGHT_TOURNAMENT_CHECKBOX_NAME,
IN_GAME_NAME_REGEXP,
USER,
} from "./user-page-constants";
@ -58,116 +53,135 @@ export const seasonsSearchParamsSchema = z.object({
.refine((nth) => !nth || Seasons.allStarted(new Date()).includes(nth)),
});
export const userEditActionSchema = z
.object({
country: z.preprocess(
falsyToNull,
z
.string()
.refine((val) => !val || COUNTRY_CODES.includes(val as any))
.nullable(),
),
bio: z.preprocess(
falsyToNull,
z.string().max(USER.BIO_MAX_LENGTH).nullable(),
),
customUrl: z.preprocess(
falsyToNull,
z
.string()
.max(USER.CUSTOM_URL_MAX_LENGTH)
.refine((val) => val === null || isCustomUrl(val), {
message: "forms.errors.invalidCustomUrl.numbers",
})
.refine((val) => val === null || /^[a-zA-Z0-9-_]+$/.test(val), {
message: "forms.errors.invalidCustomUrl.strangeCharacter",
})
.transform((val) => val?.toLowerCase())
.nullable(),
),
customName: safeNullableStringSchema({ max: USER.CUSTOM_NAME_MAX_LENGTH }),
battlefy: z.preprocess(
falsyToNull,
z.string().max(USER.BATTLEFY_MAX_LENGTH).nullable(),
),
stickSens: z.preprocess(
processMany(actualNumber, undefinedToNull),
z
.number()
.min(-50)
.max(50)
.refine((val) => val % 5 === 0)
.nullable(),
),
motionSens: z.preprocess(
processMany(actualNumber, undefinedToNull),
z
.number()
.min(-50)
.max(50)
.refine((val) => val % 5 === 0)
.nullable(),
),
subjectPronoun: z.preprocess(
processMany(nullLiteraltoNull, falsyToNull),
z.enum(SUBJECT_PRONOUNS).nullable(),
),
objectPronoun: z.preprocess(
processMany(nullLiteraltoNull, falsyToNull),
z.enum(OBJECT_PRONOUNS).nullable(),
),
inGameNameText: z.preprocess(
falsyToNull,
z.string().max(USER.IN_GAME_NAME_TEXT_MAX_LENGTH).nullable(),
),
inGameNameDiscriminator: z.preprocess(
falsyToNull,
z
.string()
.refine((val) => /^[0-9a-z]{4,5}$/.test(val))
.nullable(),
),
css: customCssVarObject,
weapons: z.preprocess(
safeJSONParse,
z
.array(
z.object({
weaponSplId,
isFavorite: dbBoolean,
}),
)
.max(USER.WEAPON_POOL_MAX_SIZE),
),
favoriteBadgeIds: z.preprocess(
processMany(safeJSONParse, emptyArrayToNull),
z
.array(id)
.min(1)
.max(BADGE.SMALL_BADGES_PER_DISPLAY_PAGE + 1)
.nullish(),
),
showDiscordUniqueName: z.preprocess(checkboxValueToDbBoolean, dbBoolean),
newProfileEnabled: z.preprocess(checkboxValueToDbBoolean, dbBoolean),
commissionsOpen: z.preprocess(checkboxValueToDbBoolean, dbBoolean),
commissionText: z.preprocess(
falsyToNull,
z.string().max(USER.COMMISSION_TEXT_MAX_LENGTH).nullable(),
),
})
const cssObjectSchema = z
.record(z.string(), z.string())
.nullable()
.refine(
(val) => {
if (val.motionSens !== null && val.stickSens === null) {
return false;
if (!val) return true;
for (const [key, value] of Object.entries(val)) {
if (!CUSTOM_CSS_VAR_COLORS.includes(key as never)) return false;
if (!/^#(?:[0-9a-fA-F]{3}){1,2}[0-9]{0,2}$/.test(value)) return false;
}
return true;
},
{
message: "forms.errors.invalidSens",
},
{ message: "Invalid custom CSS colors" },
);
const SENS_ITEMS = [
-50, -45, -40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35,
40, 45, 50,
].map((val) => ({
label: () => rawSensToString(val),
value: String(val),
}));
export const userEditProfileBaseSchema = z.object({
css: customField({ initialValue: null }, cssObjectSchema),
customName: textFieldOptional({
label: "labels.profileCustomName",
bottomText: "bottomTexts.profileCustomName",
maxLength: USER.CUSTOM_NAME_MAX_LENGTH,
}),
customUrl: textFieldOptional({
label: "labels.profileCustomUrl",
bottomText: "bottomTexts.profileCustomUrl",
leftAddon: "https://sendou.ink/u/",
maxLength: USER.CUSTOM_URL_MAX_LENGTH,
toLowerCase: true,
regExp: {
pattern: /^[a-zA-Z0-9-_]+$/,
message: "forms:errors.profileCustomUrlStrangeChar",
},
validate: {
func: isCustomUrl,
message: "forms:errors.profileCustomUrlNumbers",
},
}),
inGameName: textFieldOptional({
label: "labels.profileInGameName",
bottomText: "bottomTexts.profileInGameName",
maxLength: 10 + 1 + 5, // 10 for name, 1 for #, 5 for discriminator
regExp: {
pattern: IN_GAME_NAME_REGEXP,
message: "forms:errors.profileInGameName",
},
}),
sensitivity: dualSelectOptional({
fields: [
{ label: "labels.profileMotionSens", items: SENS_ITEMS },
{ label: "labels.profileStickSens", items: SENS_ITEMS },
],
validate: {
func: ([motion, stick]) => {
if (motion !== null && stick === null) return false;
return true;
},
message: "errors.profileSensBothOrNeither",
},
}),
pronouns: dualSelectOptional({
bottomText: "bottomTexts.profilePronouns",
fields: [
{
label: "labels.pronoun",
items: SUBJECT_PRONOUNS.map((p) => ({ label: () => p, value: p })),
},
{
label: "labels.pronoun",
items: OBJECT_PRONOUNS.map((p) => ({ label: () => p, value: p })),
},
],
validate: {
func: ([subject, object]) => {
if (subject === null && object === null) return true;
if (subject !== null && object !== null) return true;
return false;
},
message: "errors.profilePronounsBothOrNeither",
},
}),
battlefy: textFieldOptional({
label: "labels.profileBattlefy",
bottomText: "bottomTexts.profileBattlefy",
leftAddon: "https://battlefy.com/users/",
maxLength: USER.BATTLEFY_MAX_LENGTH,
}),
country: selectDynamicOptional({
label: "labels.profileCountry",
searchable: true,
}),
favoriteBadgeIds: badges({
label: "labels.profileFavoriteBadges",
maxCount: BADGE.SMALL_BADGES_PER_DISPLAY_PAGE + 1,
}),
weapons: weaponPool({
label: "labels.weaponPool",
maxCount: USER.WEAPON_POOL_MAX_SIZE,
}),
bio: textAreaOptional({
label: "labels.bio",
maxLength: USER.BIO_MAX_LENGTH,
}),
showDiscordUniqueName: toggle({
label: "labels.profileShowDiscordUniqueName",
bottomText: "bottomTexts.profileShowDiscordUniqueName",
}),
commissionsOpen: toggle({
label: "labels.profileCommissionsOpen",
bottomText: "bottomTexts.profileCommissionsOpen",
}),
commissionText: textAreaOptional({
label: "labels.profileCommissionText",
bottomText: "bottomTexts.profileCommissionText",
maxLength: USER.COMMISSION_TEXT_MAX_LENGTH,
}),
newProfileEnabled: toggle({
label: "labels.profileNewProfileEnabled",
bottomText: "bottomTexts.profileNewProfileEnabled",
}),
});
export const editHighlightsActionSchema = z.object({
[HIGHLIGHT_CHECKBOX_NAME]: z.optional(
z.union([z.array(z.string()), z.string()]),

View File

@ -43,6 +43,8 @@ export type { CustomFieldRenderProps };
interface FormFieldProps {
name: string;
label?: string;
disabled?: boolean;
maxCount?: number;
field?: z.ZodType;
children?:
| ((props: CustomFieldRenderProps) => React.ReactNode)
@ -54,6 +56,8 @@ interface FormFieldProps {
export function FormField({
name,
label,
disabled,
maxCount,
field,
children,
options,
@ -134,6 +138,7 @@ export function FormField({
<InputFormField
{...commonProps}
{...formField}
disabled={disabled}
value={value as string}
onChange={handleChange as (v: string) => void}
/>
@ -145,6 +150,7 @@ export function FormField({
<SwitchFormField
{...commonProps}
{...formField}
isDisabled={disabled}
checked={value as boolean}
onChange={handleChange as (v: boolean) => void}
/>
@ -156,6 +162,7 @@ export function FormField({
<TextareaFormField
{...commonProps}
{...formField}
disabled={disabled}
value={value as string}
onChange={handleChange as (v: string) => void}
/>
@ -285,7 +292,6 @@ export function FormField({
}
if (formField.type === "array") {
// @ts-expect-error Type instantiation is excessively deep and possibly infinite
const innerFieldMeta = formRegistry.get(formField.field) as
| FormFieldType
| undefined;
@ -372,6 +378,7 @@ export function FormField({
value={value as number[]}
onChange={handleChange as (v: number[]) => void}
options={options as BadgeOption[]}
{...(maxCount !== undefined ? { maxCount } : {})}
/>
);
}

View File

@ -415,6 +415,7 @@ function buildInitialValues<T extends z.ZodRawShape>(
const result: Record<string, unknown> = {};
for (const [key, fieldSchema] of Object.entries(schema.shape)) {
// @ts-expect-error Type instantiation is excessively deep with complex schemas
const formField = formRegistry.get(fieldSchema as z.ZodType) as
| FormField
| undefined;

View File

@ -4,6 +4,7 @@ import { ariaAttributes } from "../utils";
import { FormFieldWrapper } from "./FormFieldWrapper";
type InputFormFieldProps = FormFieldProps<"text-field"> & {
disabled?: boolean;
value: string;
onChange: (value: string) => void;
};
@ -18,6 +19,7 @@ export function InputFormField({
onBlur,
required,
inputType = "text",
disabled,
value,
onChange,
}: InputFormFieldProps) {
@ -32,7 +34,7 @@ export function InputFormField({
error={error}
bottomText={bottomText}
>
<div className={leftAddon ? "input-with-addon" : undefined}>
<div className={leftAddon ? "input-container" : undefined}>
{leftAddon ? <span className="input-addon">{leftAddon}</span> : null}
<input
id={id}
@ -41,6 +43,7 @@ export function InputFormField({
onChange={(e) => onChange(e.target.value)}
onBlur={() => onBlur?.()}
maxLength={maxLength}
disabled={disabled}
{...ariaAttributes({
id,
bottomText,

View File

@ -0,0 +1,3 @@
.searchable {
--select-width: 100%;
}

View File

@ -1,12 +1,18 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
import type { FormFieldItems, FormFieldProps } from "../types";
import { ariaAttributes } from "../utils";
import { FormFieldWrapper } from "./FormFieldWrapper";
import {
FormFieldMessages,
FormFieldWrapper,
useTranslatedTexts,
} from "./FormFieldWrapper";
import styles from "./SelectFormField.module.css";
type SelectFormFieldProps<V extends string> = Omit<
FormFieldProps<"select">,
"items" | "clearable" | "onBlur" | "name"
"items" | "clearable" | "onBlur" | "name" | "searchable"
> & {
name?: string;
items: FormFieldItems<V>;
@ -15,6 +21,7 @@ type SelectFormFieldProps<V extends string> = Omit<
onSelect?: (value: V) => void;
onBlur?: () => void;
clearable?: boolean;
searchable?: boolean;
};
export function SelectFormField<V extends string>({
@ -28,8 +35,9 @@ export function SelectFormField<V extends string>({
onChange,
onSelect,
clearable,
searchable,
}: SelectFormFieldProps<V>) {
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation(["common"]);
const id = React.useId();
const itemsWithResolvedLabels = items.map((item) => {
@ -47,6 +55,23 @@ export function SelectFormField<V extends string>({
};
});
if (searchable) {
return (
<SearchableSelect
name={name}
label={label}
bottomText={bottomText}
error={error}
items={itemsWithResolvedLabels}
value={value}
onChange={onChange}
onBlur={onBlur}
clearable={clearable}
searchPlaceholder={t("common:actions.search")}
/>
);
}
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newValue = e.target.value === "" ? null : (e.target.value as V);
onChange(newValue);
@ -81,3 +106,58 @@ export function SelectFormField<V extends string>({
</FormFieldWrapper>
);
}
function SearchableSelect<V extends string>({
name,
label,
bottomText,
error,
items,
value,
onChange,
onBlur,
clearable,
searchPlaceholder,
}: {
name?: string;
label?: string;
bottomText?: string;
error?: string;
items: Array<{ value: V; resolvedLabel: string }>;
value: V | null;
onChange: (value: V | null) => void;
onBlur?: () => void;
clearable?: boolean;
searchPlaceholder: string;
}) {
const { translatedLabel } = useTranslatedTexts({ label });
const selectItems = items.map((item) => ({
id: item.value,
textValue: item.resolvedLabel,
}));
return (
<div className={styles.searchable}>
<SendouSelect
label={translatedLabel}
selectedKey={value}
onSelectionChange={(key) => {
const newValue = key === "" ? null : (key as V);
onChange(newValue);
onBlur?.();
}}
items={selectItems}
search={{ placeholder: searchPlaceholder }}
clearable={clearable}
>
{(item) => (
<SendouSelectItem id={item.id} textValue={item.textValue}>
{item.textValue}
</SendouSelectItem>
)}
</SendouSelect>
<FormFieldMessages name={name} error={error} bottomText={bottomText} />
</div>
);
}

View File

@ -4,6 +4,7 @@ import { ariaAttributes } from "../utils";
import { FormFieldWrapper } from "./FormFieldWrapper";
type TextareaFormFieldProps = FormFieldProps<"text-area"> & {
disabled?: boolean;
value: string;
onChange: (value: string) => void;
};
@ -15,6 +16,7 @@ export function TextareaFormField({
maxLength,
error,
onBlur,
disabled,
value,
onChange,
}: TextareaFormFieldProps) {
@ -36,6 +38,7 @@ export function TextareaFormField({
value={value}
onChange={(e) => onChange(e.target.value)}
onBlur={() => onBlur?.()}
disabled={disabled}
{...ariaAttributes({
id,
bottomText,

View File

@ -60,6 +60,7 @@ export interface FormFieldSelect<T extends string, V extends string>
extends FormFieldBase<T> {
items: FormFieldItems<V>;
clearable: boolean;
searchable?: boolean;
}
type FormFieldDualSelectField<T extends string, V extends string> = Omit<
@ -142,6 +143,7 @@ interface FormFieldBadges<T extends string> extends FormFieldBase<T> {
interface FormFieldSelectDynamic<T extends string> extends FormFieldBase<T> {
clearable: boolean;
searchable?: boolean;
}
interface FormFieldStageSelect<T extends string> extends FormFieldBase<T> {
@ -241,6 +243,8 @@ export type TypedFormFieldProps<
> = {
name: TName;
label?: string;
disabled?: boolean;
maxCount?: number;
children?:
| ((props: FormFieldChildrenProps) => React.ReactNode)
| ((props: ArrayItemRenderContext) => React.ReactNode);
@ -255,6 +259,8 @@ type NestedPath = `${string}.${string}` | `${string}[${string}`;
export type FlexibleFormFieldProps = {
name: NestedPath;
label?: string;
disabled?: boolean;
maxCount?: number;
children?:
| ((props: FormFieldChildrenProps) => React.ReactNode)
| ((props: ArrayItemRenderContext) => React.ReactNode);

View File

@ -6,34 +6,10 @@
gap: var(--s-6);
}
.inGameNameText {
max-width: 8rem;
}
.inGameNameHashtag {
font-size: var(--fonts-lg);
}
.inGameNameDiscriminator {
width: 5rem;
}
.sensSelect {
width: 6rem;
}
.seperator {
margin-left: var(--s-4);
}
.weaponPool {
width: 20rem;
}
.bioContainer {
width: 100%;
}
.bioContainer > textarea {
width: 100%;
}

View File

@ -1,7 +1,3 @@
export function errorIsSqliteUniqueConstraintFailure(error: any) {
return error?.code === "SQLITE_CONSTRAINT_UNIQUE";
}
export function errorIsSqliteForeignKeyConstraintFailure(
error: unknown,
): error is Error {

View File

@ -245,7 +245,7 @@ export function nullLiteraltoNull(value: unknown): unknown {
return value;
}
export function undefinedToNull(value: unknown): unknown {
function undefinedToNull(value: unknown): unknown {
if (value === undefined) return null;
return value;

View File

@ -45,7 +45,6 @@ test.describe("Scrims", () => {
});
await page.getByLabel("Visibility").selectOption("2");
// Schema-defined field - use form helper
await form.fill("postText", "Test scrim");
await submit(page);

View File

@ -1,16 +1,17 @@
import type { Page } from "@playwright/test";
import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
import { userEditProfileBaseSchema } from "~/features/user-page/user-page-schemas";
import {
expect,
impersonate,
isNotVisible,
navigate,
seed,
selectWeapon,
submit,
test,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import { userEditProfilePage, userPage } from "~/utils/urls";
const goToEditPage = (page: Page) =>
@ -95,23 +96,20 @@ test.describe("User page", () => {
await page.getByTestId("flag-FI").isVisible();
await goToEditPage(page);
await page
.getByRole("textbox", { name: "In game name", exact: true })
.fill("Lean");
await page
.getByRole("textbox", { name: "In game name discriminator" })
.fill("1234");
const form = createFormHelpers(page, userEditProfileBaseSchema);
await form.fill("inGameName", "Lean#1234");
await page.getByLabel("R-stick sens").selectOption("0");
await page.getByLabel("Motion sens").selectOption("-50");
await page.getByLabel("Country").click();
await page.getByPlaceholder("Search countries").fill("Sweden");
await page.getByRole("searchbox", { name: "Search" }).fill("Sweden");
await page.getByRole("option", { name: "Sweden" }).click();
await page.getByLabel("Bio").fill("My awesome bio");
await submitEditForm(page);
await form.fill("bio", "My awesome bio");
await form.submit();
await page.getByTestId("flag-SV").isVisible();
await page.getByTestId("flag-SE").isVisible();
await page.getByText("My awesome bio").isVisible();
await page.getByText("Lean#1234").isVisible();
await page.getByText("Stick 0 / Motion -5").isVisible();
@ -180,11 +178,16 @@ test.describe("User page", () => {
}
await goToEditPage(page);
await selectWeapon({ name: "Range Blaster", page });
await page.getByText("Max weapon count reached").isVisible();
await page.getByTestId("delete-weapon-1100").click();
await submitEditForm(page);
const form = createFormHelpers(page, userEditProfileBaseSchema);
await form.selectWeapons("weapons", ["Range Blaster"]);
await page
.getByRole("button", { name: /Inkbrush/ })
.getByRole("button", { name: "Delete" })
.click();
await form.submit();
for (const [i, id] of [200, 2000, 4000, 220].entries()) {
await expect(page.getByTestId(`${id}-${i + 1}`)).toBeVisible();

View File

@ -1,7 +1,7 @@
{
"submit": "Fremlæg",
"labels.name": "Navn",
"labels.bio": "Bio",
"labels.bio": "Biografi",
"labels.tag": "",
"labels.teamBsky": "",
"labels.clockFormat": "",
@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Holdnavnet er taget af et andet hold",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Våbenpulje",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "Brugerdefineret Navn",
"labels.profileCustomUrl": "Brugerdefineret URL",
"labels.profileInGameName": "Splatoon 3 Brugernavn",
"labels.profileBattlefy": "Battlefy brugernavn",
"labels.profileMotionSens": "Bevægelsesfølsomhed",
"labels.profileStickSens": "Styrepindsfølsomhed",
"labels.profileCountry": "Land",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Vis Discord-brugernavn",
"labels.profileCommissionsOpen": "Åben for bestillinger",
"labels.profileCommissionText": "info om bestilling",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "Hvis feltet ikke udfyldes bruges dit discordbrugernavn: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "Battlefy-brugernavn bruges til seeding og bekræftelse i nogle turneringer",
"bottomTexts.profileShowDiscordUniqueName": "Vil du gøre dit unikke Discord-brugernavn ({{discordUniqueName}}) synligt for offentligheden?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Pris, åbne pladser eller andre relevante informationer der er relateret til at afgive en bestilling til dig.",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Brugerdefineret URL må ikke indeholde specialtegn (Gælder også æ, ø og å)",
"errors.profileCustomUrlNumbers": "Brugerdefineret URL må ikke kun indeholde numre",
"errors.profileCustomUrlDuplicate": "Brugerdefineret URL er allerede i brug",
"errors.profileSensBothOrNeither": "Bevægelsesfølsomhed kan ikke indstilles før at Styrepindsfølsomheden er indstillet",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Brugerdefineret URL",
"customName": "Brugerdefineret Navn",
"ign": "Splatoon 3 Brugernavn",
"ign.short": "Splatnavn",
"country": "Land",
"bio": "Biografi",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Bevægelse",
"stick": "Styrepind",
"sens": "Følsomhed",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Våbenpulje",
"discordExplanation": "Brugernavn, Profilbillede, Youtube-, Bluesky- og Twitch-konter er hentet via din Discord-konto. Se <1>FAQ</1> for yderligere information.",
"favoriteBadges": "",
"battlefy": "Battlefy brugernavn",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Vis Discord-brugernavn",
"forms.showDiscordUniqueName.info": "Vil du gøre dit unikke Discord-brugernavn ({{discordUniqueName}}) synligt for offentligheden?",
"forms.commissionsOpen": "Åben for bestillinger",
"forms.commissionsOpen.info": "",
"forms.commissionText": "info om bestilling",
"forms.commissionText.info": "Pris, åbne pladser eller andre relevante informationer der er relateret til at afgive en bestilling til dig.",
"forms.customName.info": "Hvis feltet ikke udfyldes bruges dit discordbrugernavn: \"{{discordName}}\"",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "Alle resultater",
"results.placing": "Placering",
"results.team": "Hold",
@ -175,12 +155,6 @@
"results.button.showHighlights": "Vis højdepunkter",
"results.button.showAll": "Vis alt",
"forms.errors.maxWeapons": "Maks antal våben nået",
"forms.errors.invalidCustomUrl.numbers": "Brugerdefineret URL må ikke kun indeholde numre",
"forms.errors.invalidCustomUrl.strangeCharacter": "Brugerdefineret URL må ikke indeholde specialtegn (Gælder også æ, ø og å)",
"forms.errors.invalidCustomUrl.duplicate": "Brugerdefineret URL er allerede i brug",
"forms.errors.invalidSens": "Bevægelsesfølsomhed kan ikke indstilles før at Styrepindsfølsomheden er indstillet",
"forms.info.customUrl": "",
"forms.info.battlefy": "Battlefy-brugernavn bruges til seeding og bekræftelse i nogle turneringer",
"search.info": "",
"search.noResults": "Søgningen {{query}} fandt ingen brugere",
"search.pleaseLogIn.header": "",

View File

@ -1,7 +1,7 @@
{
"submit": "Senden",
"labels.name": "Name",
"labels.bio": "Bio",
"labels.bio": "Über mich",
"labels.tag": "",
"labels.teamBsky": "",
"labels.clockFormat": "",
@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Es gibt bereits ein Team mit diesem Namen",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Waffenpool",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "Benutzerdefinierte URL",
"labels.profileInGameName": "Name im Spiel",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "Empfindlichkeit Bewegungssteuerung",
"labels.profileStickSens": "Empfindlichkeit R-Stick",
"labels.profileCountry": "Land",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "",
"labels.profileCommissionsOpen": "",
"labels.profileCommissionText": "",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Benutzerdefinierte URL kann nicht aus speziellen Zeichen bestehen",
"errors.profileCustomUrlNumbers": "Benutzerdefinierte URL kann nicht nur aus Zahlen bestehen",
"errors.profileCustomUrlDuplicate": "Diese Benutzerdefinierte URL wird bereits verwendet",
"errors.profileSensBothOrNeither": "Empfindlichkeit der Bewegungssteuerung kann nur festgelegt werden, wenn Empfindlichkeit R-Stick festgelegt ist",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Benutzerdefinierte URL",
"customName": "",
"ign": "Name im Spiel",
"ign.short": "IGN",
"country": "Land",
"bio": "Über mich",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Bewegungssteuerung",
"stick": "Stick",
"sens": "Empfindlichkeit",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Waffenpool",
"discordExplanation": "Der Username, Profilbild, YouTube-, Bluesky- und Twitch-Konten stammen von deinem Discord-Konto. Mehr Infos in den <1>FAQ</1>.",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "",
"forms.showDiscordUniqueName.info": "",
"forms.commissionsOpen": "",
"forms.commissionsOpen.info": "",
"forms.commissionText": "",
"forms.commissionText.info": "",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "Platzierung",
"results.team": "Team",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "Maximale Zahl an Waffen erreicht",
"forms.errors.invalidCustomUrl.numbers": "Benutzerdefinierte URL kann nicht nur aus Zahlen bestehen",
"forms.errors.invalidCustomUrl.strangeCharacter": "Benutzerdefinierte URL kann nicht aus speziellen Zeichen bestehen",
"forms.errors.invalidCustomUrl.duplicate": "Diese Benutzerdefinierte URL wird bereits verwendet",
"forms.errors.invalidSens": "Empfindlichkeit der Bewegungssteuerung kann nur festgelegt werden, wenn Empfindlichkeit R-Stick festgelegt ist",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "Keine Nutzer gefunden, die '{{query}}' entsprechen",
"search.pleaseLogIn.header": "",

View File

@ -187,5 +187,33 @@
"labels.comment": "Comment",
"errors.plusAlreadySuggested": "This user has already been suggested",
"errors.plusAlreadyMember": "This user is already a member of this tier",
"errors.plusCannotSuggest": "Can't make a suggestion right now"
"errors.plusCannotSuggest": "Can't make a suggestion right now",
"labels.profileCustomName": "Custom name",
"labels.profileCustomUrl": "Custom URL",
"labels.profileInGameName": "In-game name",
"labels.profileBattlefy": "Battlefy account name",
"labels.profileMotionSens": "Motion sens",
"labels.profileStickSens": "R-stick sens",
"labels.profileCountry": "Country",
"labels.profileFavoriteBadges": "Favorite badges",
"labels.profileShowDiscordUniqueName": "Show Discord username",
"labels.profileCommissionsOpen": "Commissions open",
"labels.profileCommissionText": "Commission info",
"labels.profileNewProfileEnabled": "New profile page",
"bottomTexts.profileCustomName": "If empty, your Discord display name is shown",
"bottomTexts.profileCustomUrl": "For patrons (Supporter & above) short link is available. E.g. instead of sendou.ink/u/sendou, snd.ink/sendou can be used.",
"bottomTexts.profileInGameName": "Format: Name#disc (e.g. Player#1234)",
"bottomTexts.profileBattlefy": "Used for seeding and verification in some tournaments",
"bottomTexts.profileShowDiscordUniqueName": "Show your Discord username publicly on your profile",
"bottomTexts.profileCommissionsOpen": "Commissions automatically close after one month",
"bottomTexts.profileCommissionText": "Price, slots open or other commission info",
"bottomTexts.profileNewProfileEnabled": "Enable the new widget-based profile page (supporter only)",
"errors.profileCustomUrlStrangeChar": "Custom URL can only contain letters, numbers, hyphens and underscores",
"errors.profileCustomUrlNumbers": "Custom URL can't only contain numbers",
"errors.profileCustomUrlDuplicate": "Someone is already using this custom URL",
"errors.profileSensBothOrNeither": "Motion sens can't be set if R-stick sens isn't",
"errors.profileInGameName": "Must match format: Name#disc (1-10 characters, #, 4-5 alphanumeric)",
"labels.pronoun": "Pronoun",
"bottomTexts.profilePronouns": "This setting is optional! Your pronouns will be displayed on your profile, tournament rosters, SendouQ groups, and text channels.",
"errors.profilePronounsBothOrNeither": "Select both pronouns or neither"
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Custom URL",
"customName": "Custom name",
"ign": "In-game name",
"ign.short": "IGN",
"country": "Country",
"bio": "Bio",
"widget.bio": "Bio",
"widget.bio-md": "Bio",
"widget.badges-owned": "Badges",
@ -143,24 +139,8 @@
"motion": "Motion",
"stick": "Stick",
"sens": "Sens",
"pronoun": "Pronoun",
"usesPronouns": "Uses",
"pronounsInfo": "This setting is optional! Your pronouns will be displayed on your profile, tournament rosters, SendouQ groups, and text channels.",
"weaponPool": "Weapon pool",
"discordExplanation": "Username, profile picture, YouTube, Bluesky and Twitch accounts come from your Discord account. See <1>FAQ</1> for more information.",
"favoriteBadges": "Favorite badges",
"battlefy": "Battlefy account name",
"forms.newProfileEnabled": "New profile page",
"forms.newProfileEnabled.info": "Enable the new widget-based profile page. Currently available as an early preview for supporters.",
"forms.showDiscordUniqueName": "Show Discord username",
"forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
"forms.commissionsOpen": "Commissions open",
"forms.commissionsOpen.info": "Commissions automatically close and need to be re-enabled after one month to prevent stale listings",
"forms.commissionText": "Commission info",
"forms.commissionText.info": "Price, slots open or other info related to commissioning you",
"forms.customName.info": "If missing, your Discord display name is used: \"{{discordName}}\"",
"forms.country.search.placeholder": "Search countries",
"forms.favoriteBadges.nonSupporter": "Become supporter to set which badges appear on the first page and the order",
"results.title": "All results",
"results.placing": "Placing",
"results.team": "Team",
@ -175,12 +155,6 @@
"results.button.showHighlights": "Show highlights",
"results.button.showAll": "Show all",
"forms.errors.maxWeapons": "Max weapon count reached",
"forms.errors.invalidCustomUrl.numbers": "Custom URL can't only contain numbers",
"forms.errors.invalidCustomUrl.strangeCharacter": "Custom URL can't contain special characters",
"forms.errors.invalidCustomUrl.duplicate": "Someone is already using this custom URL",
"forms.errors.invalidSens": "Motion sens can't be set if R-stick sens isn't",
"forms.info.customUrl": "For patrons (Supporter & above) short link is available. E.g. instead of sendou.ink/u/sendou, snd.ink/sendou can be used.",
"forms.info.battlefy": "Battlefy account name is used for seeding and verification in some tournaments",
"search.info": "Search for users by Discord or Splatoon 3 name",
"search.noResults": "No users found matching '{{query}}'",
"search.pleaseLogIn.header": "Please log in to search for users",

View File

@ -1,7 +1,7 @@
{
"submit": "Finalizar",
"labels.name": "Nombre",
"labels.bio": "Bio",
"labels.bio": "Biografía",
"labels.tag": "",
"labels.teamBsky": "",
"labels.clockFormat": "",
@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Ya existe un equipo con ese nombre",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Grupo de armas",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "Enlace personalizado",
"labels.profileInGameName": "Nombre en el juego",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "Sens del giroscopio",
"labels.profileStickSens": "Sens de palanca",
"labels.profileCountry": "País",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Mostrar usuario de Discord",
"labels.profileCommissionsOpen": "Comisiones abiertas",
"labels.profileCommissionText": "Info de comisiones",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "¿Mostrar tu nombre de Discord ({{discordUniqueName}}) publicamente?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Precio, espacios abiertos, o cualquier otra información sobre tus comiciones.",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Enlace personalizado no puede contener caracteres especiales",
"errors.profileCustomUrlNumbers": "Enlace personalizado no puede ser solo numeros",
"errors.profileCustomUrlDuplicate": "Alguien ya tiene ese enlace personalizado",
"errors.profileSensBothOrNeither": "Motion sens can't be set if R-stick sens isn't",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Enlace personalizado",
"customName": "",
"ign": "Nombre en el juego",
"ign.short": "IGN",
"country": "País",
"bio": "Biografía",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Giroscopio",
"stick": "Palanca",
"sens": "Sens",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Grupo de armas",
"discordExplanation": "Tu nombre, foto, y cuentas de YouTube, Bluesky y Twitch se obtienen por tu cuenta en Discord. Ver <1>FAQ</1> para más información.",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Mostrar usuario de Discord",
"forms.showDiscordUniqueName.info": "¿Mostrar tu nombre de Discord ({{discordUniqueName}}) publicamente?",
"forms.commissionsOpen": "Comisiones abiertas",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Info de comisiones",
"forms.commissionText.info": "Precio, espacios abiertos, o cualquier otra información sobre tus comiciones.",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "Lugar",
"results.team": "Equipo",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "Máxima cantidad de armas",
"forms.errors.invalidCustomUrl.numbers": "Enlace personalizado no puede ser solo numeros",
"forms.errors.invalidCustomUrl.strangeCharacter": "Enlace personalizado no puede contener caracteres especiales",
"forms.errors.invalidCustomUrl.duplicate": "Alguien ya tiene ese enlace personalizado",
"forms.errors.invalidSens": "Motion sens can't be set if R-stick sens isn't",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -1,7 +1,7 @@
{
"submit": "Finalizar",
"labels.name": "Nombre",
"labels.bio": "Bio",
"labels.bio": "Biografía",
"labels.tag": "",
"labels.teamBsky": "",
"labels.clockFormat": "",
@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Ya existe un equipo con ese nombre",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Grupo de armas",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "Nombre personalizado",
"labels.profileCustomUrl": "Enlace personalizado",
"labels.profileInGameName": "Nombre en el juego",
"labels.profileBattlefy": "Nombre de cuenta de Battlefy",
"labels.profileMotionSens": "Sens del giroscopio",
"labels.profileStickSens": "Sens de palanca",
"labels.profileCountry": "País",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Mostrar usuario de Discord",
"labels.profileCommissionsOpen": "Comisiones abiertas",
"labels.profileCommissionText": "Info de comisiones",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "Si vacío, se mostrará tu nombre de Discord: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "El nombre de tu cuenta de Battlefy se utiliza para la clasificación y verificación en algunos torneos.",
"bottomTexts.profileShowDiscordUniqueName": "¿Mostrar tu nombre de Discord ({{discordUniqueName}}) publicamente?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Precio, espacios abiertos, o cualquier otra información sobre tus comiciones.",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Enlace personalizado no puede contener caracteres especiales",
"errors.profileCustomUrlNumbers": "Enlace personalizado no puede ser solo numeros",
"errors.profileCustomUrlDuplicate": "Alguien ya tiene ese enlace personalizado",
"errors.profileSensBothOrNeither": "Sens de giroscopio no se poner sin la sens de palanca",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Enlace personalizado",
"customName": "Nombre personalizado",
"ign": "Nombre en el juego",
"ign.short": "IGN",
"country": "País",
"bio": "Biografía",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Giroscopio",
"stick": "Palanca",
"sens": "Sens",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Grupo de armas",
"discordExplanation": "Tu nombre, foto, y cuentas de YouTube, Bluesky y Twitch se obtienen por tu cuenta en Discord. Ver <1>FAQ</1> para más información.",
"favoriteBadges": "",
"battlefy": "Nombre de cuenta de Battlefy",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Mostrar usuario de Discord",
"forms.showDiscordUniqueName.info": "¿Mostrar tu nombre de Discord ({{discordUniqueName}}) publicamente?",
"forms.commissionsOpen": "Comisiones abiertas",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Info de comisiones",
"forms.commissionText.info": "Precio, espacios abiertos, o cualquier otra información sobre tus comiciones.",
"forms.customName.info": "Si vacío, se mostrará tu nombre de Discord: \"{{discordName}}\"",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "Todos los resultados",
"results.placing": "Lugar",
"results.team": "Equipo",
@ -175,12 +155,6 @@
"results.button.showHighlights": "Mostrar resaltos",
"results.button.showAll": "Mostrar todos",
"forms.errors.maxWeapons": "Máxima cantidad de armas",
"forms.errors.invalidCustomUrl.numbers": "Enlace personalizado no puede ser solo numeros",
"forms.errors.invalidCustomUrl.strangeCharacter": "Enlace personalizado no puede contener caracteres especiales",
"forms.errors.invalidCustomUrl.duplicate": "Alguien ya tiene ese enlace personalizado",
"forms.errors.invalidSens": "Sens de giroscopio no se poner sin la sens de palanca",
"forms.info.customUrl": "",
"forms.info.battlefy": "El nombre de tu cuenta de Battlefy se utiliza para la clasificación y verificación en algunos torneos.",
"search.info": "",
"search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Il y a déjà une équipe avec ce nom",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Armes jouées",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "URL personnalisée",
"labels.profileInGameName": "Pseudo en jeu",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "Sensibilité du gyroscope",
"labels.profileStickSens": "Sensibilité du stick droit",
"labels.profileCountry": "Pays",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Montrer le pseudo Discord",
"labels.profileCommissionsOpen": "Commissions acceptées",
"labels.profileCommissionText": "Info pour les commissions",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Prix, disponibilités et tout autres info nécéssaires",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Votre URL personnalisée ne peut pas contenir de caractères spéciaux",
"errors.profileCustomUrlNumbers": "Votre URL personnalisée ne peut pas contenir que des nombres",
"errors.profileCustomUrlDuplicate": "Cette URL a déjà été choisie par quelqu'un",
"errors.profileSensBothOrNeither": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "URL personnalisée",
"customName": "",
"ign": "Pseudo en jeu",
"ign.short": "PEJ",
"country": "Pays",
"bio": "Bio",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Gyro",
"stick": "Stick",
"sens": "Sens",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Armes jouées",
"discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Bluesky et Twitch viennent de votre compte Discord. Voir la <1>FAQ</1> pour plus d'informations.",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Montrer le pseudo Discord",
"forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
"forms.commissionsOpen": "Commissions acceptées",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Info pour les commissions",
"forms.commissionText.info": "Prix, disponibilités et tout autres info nécéssaires",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "Placement",
"results.team": "Équipe",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "Nombre d'armes maximum atteint",
"forms.errors.invalidCustomUrl.numbers": "Votre URL personnalisée ne peut pas contenir que des nombres",
"forms.errors.invalidCustomUrl.strangeCharacter": "Votre URL personnalisée ne peut pas contenir de caractères spéciaux",
"forms.errors.invalidCustomUrl.duplicate": "Cette URL a déjà été choisie par quelqu'un",
"forms.errors.invalidSens": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé",
"search.pleaseLogIn.header": "",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Il y a déjà une équipe avec ce nom",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Armes jouées",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "Nom personnalisée",
"labels.profileCustomUrl": "URL personnalisée",
"labels.profileInGameName": "Pseudo en jeu",
"labels.profileBattlefy": "Nom du compte Battlefy",
"labels.profileMotionSens": "Sensibilité du gyroscope",
"labels.profileStickSens": "Sensibilité du stick droit",
"labels.profileCountry": "Pays",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Montrer le pseudo Discord",
"labels.profileCommissionsOpen": "Commissions acceptées",
"labels.profileCommissionText": "Info pour les commissions",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "Si il n'est pas présent, votre pseudo discord est utilisé: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "Pour les Supporter patrons (& plus), les liens courts sont disponibles. Exemple: Au mieux de sendou.ink/u/sendou, snd.ink/sendou peut être utilisé.",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "Votre nom Battlefy est utiliser pour le seeding et la verification de certains tournois",
"bottomTexts.profileShowDiscordUniqueName": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Prix, disponibilités et tout autres info nécéssaires",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Votre URL personnalisée ne peut pas contenir de caractères spéciaux",
"errors.profileCustomUrlNumbers": "Votre URL personnalisée ne peut pas contenir que des nombres",
"errors.profileCustomUrlDuplicate": "Cette URL a déjà été choisie par quelqu'un",
"errors.profileSensBothOrNeither": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "URL personnalisée",
"customName": "Nom personnalisée",
"ign": "Pseudo en jeu",
"ign.short": "PEJ",
"country": "Pays",
"bio": "Bio",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Gyro",
"stick": "Stick",
"sens": "Sens",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Armes jouées",
"discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Bluesky et Twitch viennent de votre compte Discord. Voir la <1>FAQ</1> pour plus d'informations.",
"favoriteBadges": "Badge favori",
"battlefy": "Nom du compte Battlefy",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Montrer le pseudo Discord",
"forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
"forms.commissionsOpen": "Commissions acceptées",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Info pour les commissions",
"forms.commissionText.info": "Prix, disponibilités et tout autres info nécéssaires",
"forms.customName.info": "Si il n'est pas présent, votre pseudo discord est utilisé: \"{{discordName}}\"",
"forms.country.search.placeholder": "Rechercher de pays",
"forms.favoriteBadges.nonSupporter": "Devenez supporter pour définir l'ordre des badges qui apparaissent sur la première page",
"results.title": "Tout les résultats",
"results.placing": "Placement",
"results.team": "Équipe",
@ -175,12 +155,6 @@
"results.button.showHighlights": "Montrer les highlights",
"results.button.showAll": "Tout montrer",
"forms.errors.maxWeapons": "Nombre d'armes maximum atteint",
"forms.errors.invalidCustomUrl.numbers": "Votre URL personnalisée ne peut pas contenir que des nombres",
"forms.errors.invalidCustomUrl.strangeCharacter": "Votre URL personnalisée ne peut pas contenir de caractères spéciaux",
"forms.errors.invalidCustomUrl.duplicate": "Cette URL a déjà été choisie par quelqu'un",
"forms.errors.invalidSens": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas",
"forms.info.customUrl": "Pour les Supporter patrons (& plus), les liens courts sont disponibles. Exemple: Au mieux de sendou.ink/u/sendou, snd.ink/sendou peut être utilisé.",
"forms.info.battlefy": "Votre nom Battlefy est utiliser pour le seeding et la verification de certains tournois",
"search.info": "Recherchez avec le pseudo Discord ou Splatoon 3 du compte",
"search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé",
"search.pleaseLogIn.header": "Veuillez vous connecter pour rechercher des utilisateurs",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "יש כבר צוות בשם הזה",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "מאגר נשקים",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "כתובת URL מותאמת אישית",
"labels.profileInGameName": "שם במשחק",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "רגישות תנועה",
"labels.profileStickSens": "רגישות סטיק ימני",
"labels.profileCountry": "מדינה",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "הראה שם משתמש Discord",
"labels.profileCommissionsOpen": "בקשות פתוחות",
"labels.profileCommissionText": "מידע עבור בקשות",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "להראות את שם ה-Discord היחודי שלכם ({{discordUniqueName}}) בפומבי?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "מחיר, כמות בקשות או מידע אחר שקשור לבקשות אלכם",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "כתובת URL מותאמת אישית לא יכולה להכיל תווים מיוחדים",
"errors.profileCustomUrlNumbers": "כתובת URL מותאמת אישית לא יכולה להכיל רק מספרים",
"errors.profileCustomUrlDuplicate": "מישהו כבר משתמש בכתובת URL המותאמת אישית הזו",
"errors.profileSensBothOrNeither": "לא ניתן להגדיר את רגישות התנועה אם רגישות הסטיק לא מוגדרת",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "כתובת URL מותאמת אישית",
"customName": "",
"ign": "שם במשחק",
"ign.short": "IGN",
"country": "מדינה",
"bio": "ביו",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "תנועה",
"stick": "סטיק",
"sens": "רגישות",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "מאגר נשקים",
"discordExplanation": "שם משתמש, תמונת פרופיל, חשבונות YouTube, Bluesky ו-Twitch מגיעים מחשבון Discord שלך. ראו <1>שאלות נפוצות</1> למידע נוסף.",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "הראה שם משתמש Discord",
"forms.showDiscordUniqueName.info": "להראות את שם ה-Discord היחודי שלכם ({{discordUniqueName}}) בפומבי?",
"forms.commissionsOpen": "בקשות פתוחות",
"forms.commissionsOpen.info": "",
"forms.commissionText": "מידע עבור בקשות",
"forms.commissionText.info": "מחיר, כמות בקשות או מידע אחר שקשור לבקשות אלכם",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "מיקום",
"results.team": "צוות",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "הגעה לכמות מקסימלית של מספר נשקים",
"forms.errors.invalidCustomUrl.numbers": "כתובת URL מותאמת אישית לא יכולה להכיל רק מספרים",
"forms.errors.invalidCustomUrl.strangeCharacter": "כתובת URL מותאמת אישית לא יכולה להכיל תווים מיוחדים",
"forms.errors.invalidCustomUrl.duplicate": "מישהו כבר משתמש בכתובת URL המותאמת אישית הזו",
"forms.errors.invalidSens": "לא ניתן להגדיר את רגישות התנועה אם רגישות הסטיק לא מוגדרת",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "לא נמצאו משתמשים התואמים '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -1,7 +1,7 @@
{
"submit": "Invia",
"labels.name": "Nome",
"labels.bio": "Bio",
"labels.bio": "Biografia",
"labels.tag": "",
"labels.teamBsky": "Bluesky del team",
"labels.clockFormat": "",
@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Esiste già un team con questo nome",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Pool armi",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "Nome personalizzato",
"labels.profileCustomUrl": "URL personalizzato",
"labels.profileInGameName": "Nome nel gioco",
"labels.profileBattlefy": "Nome account Battlefy",
"labels.profileMotionSens": "Sensitività Giroscopio",
"labels.profileStickSens": "Sensitività Joystick Dx",
"labels.profileCountry": "Paese",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Mostra username Discord",
"labels.profileCommissionsOpen": "Commissioni aperte",
"labels.profileCommissionText": "Info sulle commissioni",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "Se mancante, viene usato il tuo nome visualizzato Discord: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "Per gli iscritti al Patreon (Supporter compreso in su) è disponibile il link corto. Es. invece di sendou.ink/u/sendou, può essere usato snd.ink/sendou.",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "Il nome dell'account Battlefy è usato per il seeding e verifica in alcuni tornei",
"bottomTexts.profileShowDiscordUniqueName": "Mostrare il proprio nome unico Discord ({{discordUniqueName}}) pubblicamente?",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Prezzo, posti liberi o altre info relative al commissionarti",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "L'URL personalizzato non può contenere caratteri speciali",
"errors.profileCustomUrlNumbers": "L'URL personalizzato non può contenere solo numeri",
"errors.profileCustomUrlDuplicate": "L'URL personalizzato è già in uso da un altro utente",
"errors.profileSensBothOrNeither": "La sensibilità del giroscopio non può essere impostata se non hai impostato la sensibilità del joystick destro",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "URL personalizzato",
"customName": "Nome personalizzato",
"ign": "Nome nel gioco",
"ign.short": "IGN",
"country": "Paese",
"bio": "Biografia",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Giroscopio",
"stick": "Joystick",
"sens": "Sens.",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Pool armi",
"discordExplanation": "Username, foto profilo, account YouTube, Bluesky e Twitch vengono dal tuo account Discord. Visita <1>FAQ</1> per ulteriori informazioni.",
"favoriteBadges": "",
"battlefy": "Nome account Battlefy",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Mostra username Discord",
"forms.showDiscordUniqueName.info": "Mostrare il proprio nome unico Discord ({{discordUniqueName}}) pubblicamente?",
"forms.commissionsOpen": "Commissioni aperte",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Info sulle commissioni",
"forms.commissionText.info": "Prezzo, posti liberi o altre info relative al commissionarti",
"forms.customName.info": "Se mancante, viene usato il tuo nome visualizzato Discord: \"{{discordName}}\"",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "Tutti i risultati",
"results.placing": "Risultato",
"results.team": "Team",
@ -175,12 +155,6 @@
"results.button.showHighlights": "Mostra highlight",
"results.button.showAll": "Mostra tutti",
"forms.errors.maxWeapons": "Massimo numero di armi raggiunto",
"forms.errors.invalidCustomUrl.numbers": "L'URL personalizzato non può contenere solo numeri",
"forms.errors.invalidCustomUrl.strangeCharacter": "L'URL personalizzato non può contenere caratteri speciali",
"forms.errors.invalidCustomUrl.duplicate": "L'URL personalizzato è già in uso da un altro utente",
"forms.errors.invalidSens": "La sensibilità del giroscopio non può essere impostata se non hai impostato la sensibilità del joystick destro",
"forms.info.customUrl": "Per gli iscritti al Patreon (Supporter compreso in su) è disponibile il link corto. Es. invece di sendou.ink/u/sendou, può essere usato snd.ink/sendou.",
"forms.info.battlefy": "Il nome dell'account Battlefy è usato per il seeding e verifica in alcuni tornei",
"search.info": "Cerca utenti tramite nome Discord o Splatoon 3",
"search.noResults": "Nessun utente trovato per '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -1,7 +1,7 @@
{
"submit": "送信",
"labels.name": "名前",
"labels.bio": "Bio",
"labels.bio": "自己紹介",
"labels.tag": "",
"labels.teamBsky": "チームの Bluesky",
"labels.clockFormat": "",
@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "そのチーム名はすでに使用されています",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "使用ブキ",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "カスタム 名前",
"labels.profileCustomUrl": "カスタム URL",
"labels.profileInGameName": "ゲーム中の名前",
"labels.profileBattlefy": "Battlefyアカウント名",
"labels.profileMotionSens": "モーション感度",
"labels.profileStickSens": "右スティック感度",
"labels.profileCountry": "国",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Discord のユーザー名を表示する",
"labels.profileCommissionsOpen": "依頼を受付中",
"labels.profileCommissionText": "依頼に関する情報",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "記入されてない場合ディスコードの表示名 \"{{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": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "カスタム URL",
"customName": "カスタム 名前",
"ign": "ゲーム中の名前",
"ign.short": "ゲーム中の名前",
"country": "国",
"bio": "自己紹介",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "モーション",
"stick": "スティック",
"sens": "感度",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "使用ブキ",
"discordExplanation": "ユーザー名、プロファイル画像、YouTube、Bluesky と Twitch アカウントは Discord のアカウントに設定されているものが使用されます。詳しくは <1>FAQ</1> をご覧ください。",
"favoriteBadges": "",
"battlefy": "Battlefyアカウント名",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Discord のユーザー名を表示する",
"forms.showDiscordUniqueName.info": "Discord のユニーク名 ({{discordUniqueName}}) 公表しますか?",
"forms.commissionsOpen": "依頼を受付中",
"forms.commissionsOpen.info": "",
"forms.commissionText": "依頼に関する情報",
"forms.commissionText.info": "価格、受付数、その他依頼に関する情報",
"forms.customName.info": "記入されてない場合ディスコードの表示名 \"{{discordName}}\"を使います",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "全ての結果",
"results.placing": "順位",
"results.team": "チーム",
@ -175,12 +155,6 @@
"results.button.showHighlights": "ハイライトを表示",
"results.button.showAll": "全て表示",
"forms.errors.maxWeapons": "最大ブキ数を超えました",
"forms.errors.invalidCustomUrl.numbers": "カスタム URL は数字のみで作成することはできません",
"forms.errors.invalidCustomUrl.strangeCharacter": "カスタム URL は特殊文字を含めることはできません",
"forms.errors.invalidCustomUrl.duplicate": "このカスタム URL はすでに使用されています",
"forms.errors.invalidSens": "右スティックの感度が設定されていない場合、感度を設定することはできません",
"forms.info.customUrl": "",
"forms.info.battlefy": "Battlefyのアカウント名は特定のトーナメンでシーディング及びにプレイヤー情報の確認に使用されます。",
"search.info": "",
"search.noResults": "該当ユーザーが見つかりません '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -1,7 +1,7 @@
{
"submit": "제출",
"labels.name": "이름",
"labels.bio": "",
"labels.bio": "소개",
"labels.tag": "",
"labels.teamBsky": "",
"labels.clockFormat": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "",
"labels.profileInGameName": "",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "",
"labels.profileStickSens": "",
"labels.profileCountry": "국가",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "",
"labels.profileCommissionsOpen": "",
"labels.profileCommissionText": "",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "",
"errors.profileCustomUrlNumbers": "",
"errors.profileCustomUrlDuplicate": "",
"errors.profileSensBothOrNeither": "",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "",
"customName": "",
"ign": "",
"ign.short": "",
"country": "국가",
"bio": "소개",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "",
"stick": "",
"sens": "",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "",
"discordExplanation": "",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "",
"forms.showDiscordUniqueName.info": "",
"forms.commissionsOpen": "",
"forms.commissionsOpen.info": "",
"forms.commissionText": "",
"forms.commissionText.info": "",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "순위",
"results.team": "팀",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "",
"forms.errors.invalidCustomUrl.numbers": "",
"forms.errors.invalidCustomUrl.strangeCharacter": "",
"forms.errors.invalidCustomUrl.duplicate": "",
"forms.errors.invalidSens": "",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "",
"search.pleaseLogIn.header": "",

View File

@ -1,7 +1,7 @@
{
"submit": "Verzenden",
"labels.name": "Naam",
"labels.bio": "",
"labels.bio": "Bio",
"labels.tag": "",
"labels.teamBsky": "",
"labels.clockFormat": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "Eigen URL",
"labels.profileInGameName": "In-game naam",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "Bewegingsgevoeligheid",
"labels.profileStickSens": "R-stick gevoeligheid",
"labels.profileCountry": "Land",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "",
"labels.profileCommissionsOpen": "",
"labels.profileCommissionText": "",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Een eigen URL mag geen speciale karakters bevatten",
"errors.profileCustomUrlNumbers": "Een eigen URL kan niet alleen uit nummers bestaan",
"errors.profileCustomUrlDuplicate": "Deze URL is al in gebruik",
"errors.profileSensBothOrNeither": "Bewegingsgevoeligheid kan niet worden ingesteld als er niets voor de R-stick ingevoerd is",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Eigen URL",
"customName": "",
"ign": "In-game naam",
"ign.short": "IGN",
"country": "Land",
"bio": "Bio",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Beweging",
"stick": "Stick",
"sens": "Gevoeligheid",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "",
"discordExplanation": "",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "",
"forms.showDiscordUniqueName.info": "",
"forms.commissionsOpen": "",
"forms.commissionsOpen.info": "",
"forms.commissionText": "",
"forms.commissionText.info": "",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "Plaatsing",
"results.team": "Team",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "",
"forms.errors.invalidCustomUrl.numbers": "Een eigen URL kan niet alleen uit nummers bestaan",
"forms.errors.invalidCustomUrl.strangeCharacter": "Een eigen URL mag geen speciale karakters bevatten",
"forms.errors.invalidCustomUrl.duplicate": "Deze URL is al in gebruik",
"forms.errors.invalidSens": "Bewegingsgevoeligheid kan niet worden ingesteld als er niets voor de R-stick ingevoerd is",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "",
"search.pleaseLogIn.header": "",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Istnieje już drużyna o tym imieniu",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Pula broni",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "Niestandardowe URL",
"labels.profileInGameName": "Imię In-game",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "Motion sens",
"labels.profileStickSens": "R-stick sens",
"labels.profileCountry": "Kraj",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "",
"labels.profileCommissionsOpen": "",
"labels.profileCommissionText": "",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "Niestandardowe URL nie może zawierać znaków specjalnych",
"errors.profileCustomUrlNumbers": "Niestandardowe URL nie może zawierać tylko liczby",
"errors.profileCustomUrlDuplicate": "Te niestandardowe URl jest już przez kogoś zajęte",
"errors.profileSensBothOrNeither": "Motion sens nie może być ustawione jeśli R-stick sens nie jest",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Niestandardowe URL",
"customName": "",
"ign": "Imię In-game",
"ign.short": "IGN",
"country": "Kraj",
"bio": "Opis",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Motion",
"stick": "Stick",
"sens": "Sens",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Pula broni",
"discordExplanation": "Nazwa, profilowe oraz połączone konta brane są z konta Discord. Zobacz <1>FAQ</1> by dowiedzieć się więcej.",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "",
"forms.showDiscordUniqueName.info": "",
"forms.commissionsOpen": "",
"forms.commissionsOpen.info": "",
"forms.commissionText": "",
"forms.commissionText.info": "",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "Placing",
"results.team": "Drużyna",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "Maksymalna ilość broni osiągnięta",
"forms.errors.invalidCustomUrl.numbers": "Niestandardowe URL nie może zawierać tylko liczby",
"forms.errors.invalidCustomUrl.strangeCharacter": "Niestandardowe URL nie może zawierać znaków specjalnych",
"forms.errors.invalidCustomUrl.duplicate": "Te niestandardowe URl jest już przez kogoś zajęte",
"forms.errors.invalidSens": "Motion sens nie może być ustawione jeśli R-stick sens nie jest",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "Nie znaleziono użytkownika o nazwie '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Já existe um time com esse nome",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Seleção de armas",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "URL personalizado",
"labels.profileInGameName": "Nome no jogo",
"labels.profileBattlefy": "",
"labels.profileMotionSens": "Sensibilidade do Controle de Movimento (Giroscópio)",
"labels.profileStickSens": "Sensibilidade do Analógico Direito",
"labels.profileCountry": "País",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Mostrar nome de usuário Discord",
"labels.profileCommissionsOpen": "Comissões abertas",
"labels.profileCommissionText": "Info sobre comissões",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",
"bottomTexts.profileBattlefy": "",
"bottomTexts.profileShowDiscordUniqueName": "Deixe ativado para mostrar seu nome de usuário único do Discord ({{discordUniqueName}}) publicamente.",
"bottomTexts.profileCommissionsOpen": "",
"bottomTexts.profileCommissionText": "Preço, vagas abertas ou qualquer outra informação relacionada ao processo de fazer um pedido para você.",
"bottomTexts.profileNewProfileEnabled": "",
"errors.profileCustomUrlStrangeChar": "URL personalizado não pode conter caracteres especiais",
"errors.profileCustomUrlNumbers": "URL personalizado não pode conter apenas números",
"errors.profileCustomUrlDuplicate": "Alguém já está usando esse URL personalizado",
"errors.profileSensBothOrNeither": "A sensibilidade de Movimento não pode ser definida se a sensibilidade do Analógico Direito não está",
"errors.profileInGameName": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "URL personalizado",
"customName": "",
"ign": "Nome no jogo",
"ign.short": "NNJ (IGN)",
"country": "País",
"bio": "Bio",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Movimento (Giroscópio)",
"stick": "Analógico",
"sens": "Sens",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Seleção de armas",
"discordExplanation": "Nome de usuário, foto de perfil, conta do YouTube, Bluesky e Twitch vêm da sua conta do Discord. Veja o <1>Perguntas Frequentes</1> para mais informações.",
"favoriteBadges": "",
"battlefy": "",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Mostrar nome de usuário Discord",
"forms.showDiscordUniqueName.info": "Deixe ativado para mostrar seu nome de usuário único do Discord ({{discordUniqueName}}) publicamente.",
"forms.commissionsOpen": "Comissões abertas",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Info sobre comissões",
"forms.commissionText.info": "Preço, vagas abertas ou qualquer outra informação relacionada ao processo de fazer um pedido para você.",
"forms.customName.info": "",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "",
"results.placing": "Classificação",
"results.team": "Time",
@ -175,12 +155,6 @@
"results.button.showHighlights": "",
"results.button.showAll": "",
"forms.errors.maxWeapons": "Número máximo de armas no perfil atingido.",
"forms.errors.invalidCustomUrl.numbers": "URL personalizado não pode conter apenas números",
"forms.errors.invalidCustomUrl.strangeCharacter": "URL personalizado não pode conter caracteres especiais",
"forms.errors.invalidCustomUrl.duplicate": "Alguém já está usando esse URL personalizado",
"forms.errors.invalidSens": "A sensibilidade de Movimento não pode ser definida se a sensibilidade do Analógico Direito não está",
"forms.info.customUrl": "",
"forms.info.battlefy": "",
"search.info": "",
"search.noResults": "Nenhum usuário encontrado com o termo '{{query}}'",
"search.pleaseLogIn.header": "",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "Уже существует команда с таким названием",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "Используемое оружие",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "Пользовательское имя",
"labels.profileCustomUrl": "Пользовательский URL",
"labels.profileInGameName": "Внутриигровое имя",
"labels.profileBattlefy": "Аккаунт Battlefy",
"labels.profileMotionSens": "Чувствительность наклона",
"labels.profileStickSens": "Чувствительность стика",
"labels.profileCountry": "Страна",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "Показать пользовательское имя Discord",
"labels.profileCommissionsOpen": "Коммишены открыты",
"labels.profileCommissionText": "Информация о коммишенах",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomName": "Если пользовательское имя отсутствует, то будет использовано ваше имя в Discord: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "Для меценатов (Supporter и выше) доступна короткая ссылка. Например, вместо sendou.ink/u/sendou может быть использована сссылка snd.ink/sendou.",
"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": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "Пользовательский URL",
"customName": "Пользовательское имя",
"ign": "Внутриигровое имя",
"ign.short": "Ник",
"country": "Страна",
"bio": "О себе",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "Наклон",
"stick": "Стик",
"sens": "Чувствительность",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "Используемое оружие",
"discordExplanation": "Имя пользователя, аватар, ссылка на аккаунты YouTube, Bluesky и Twitch берутся из вашего аккаунта в Discord. Посмотрите <1>FAQ</1> для дополнительной информации.",
"favoriteBadges": "Любимые награды",
"battlefy": "Аккаунт Battlefy",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "Показать пользовательское имя Discord",
"forms.showDiscordUniqueName.info": "Показывать ваше уникальное Discord имя ({{discordUniqueName}})?",
"forms.commissionsOpen": "Коммишены открыты",
"forms.commissionsOpen.info": "",
"forms.commissionText": "Информация о коммишенах",
"forms.commissionText.info": "Цена, слоты и другая информация о ваших коммишенах",
"forms.customName.info": "Если пользовательское имя отсутствует, то будет использовано ваше имя в Discord: \"{{discordName}}\"",
"forms.country.search.placeholder": "Искать страны",
"forms.favoriteBadges.nonSupporter": "Станьте суппортером sendou.ink, чтобы выбрать какие награды и в каком порядке отображаются на первой странице",
"results.title": "Все результаты",
"results.placing": "Место",
"results.team": "Команда",
@ -175,12 +155,6 @@
"results.button.showHighlights": "Показать избранные",
"results.button.showAll": "Показать все",
"forms.errors.maxWeapons": "Достигнут максимум",
"forms.errors.invalidCustomUrl.numbers": "Пользовательский URL не может содержать только цифры",
"forms.errors.invalidCustomUrl.strangeCharacter": "Пользовательский URL не может содержать особые символы",
"forms.errors.invalidCustomUrl.duplicate": "Кто-то уже использует этот пользовательский URL",
"forms.errors.invalidSens": "Чувствительность наклона не может быть указана, если не указана чувствительность стика",
"forms.info.customUrl": "Для меценатов (Supporter и выше) доступна короткая ссылка. Например, вместо sendou.ink/u/sendou может быть использована сссылка snd.ink/sendou.",
"forms.info.battlefy": "Имя на Battlefy может быть использовано для посева и верификации в некоторых турнирах",
"search.info": "Поиск пользователей по имени Discord или Splatoon 3",
"search.noResults": "По запросу '{{query}}' пользователь не найден",
"search.pleaseLogIn.header": "Пожалуйста, войдите в аккаунт для поиска пользователя",

View File

@ -24,7 +24,7 @@
"errors.atLeastOneOption": "",
"errors.duplicateName": "该队名已被使用",
"errors.noOnlySpecialCharacters": "",
"labels.weaponPool": "",
"labels.weaponPool": "武器池",
"placeholders.weaponPoolFull": "",
"labels.voiceChat": "",
"labels.languages": "",
@ -187,5 +187,33 @@
"labels.comment": "",
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": ""
"errors.plusCannotSuggest": "",
"labels.profileCustomName": "自定义昵称",
"labels.profileCustomUrl": "自定义URL",
"labels.profileInGameName": "游戏ID",
"labels.profileBattlefy": "Battlefy用户名",
"labels.profileMotionSens": "体感感度",
"labels.profileStickSens": "摇杆感度",
"labels.profileCountry": "国家",
"labels.profileFavoriteBadges": "",
"labels.profileShowDiscordUniqueName": "显示Discord用户名",
"labels.profileCommissionsOpen": "开放委托",
"labels.profileCommissionText": "委托信息",
"labels.profileNewProfileEnabled": "",
"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": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": ""
}

View File

@ -1,10 +1,6 @@
{
"customUrl": "自定义URL",
"customName": "自定义昵称",
"ign": "游戏ID",
"ign.short": "游戏ID",
"country": "国家",
"bio": "个人简介",
"widget.bio": "",
"widget.bio-md": "",
"widget.badges-owned": "",
@ -143,24 +139,8 @@
"motion": "体感",
"stick": "摇杆",
"sens": "感度",
"pronoun": "",
"usesPronouns": "",
"pronounsInfo": "",
"weaponPool": "武器池",
"discordExplanation": "用户名、头像、Youtube、Bluesky和Twitch账号皆来自您的Discord账号。查看 <1>FAQ</1> 以获得更多相关信息。",
"favoriteBadges": "",
"battlefy": "Battlefy用户名",
"forms.newProfileEnabled": "",
"forms.newProfileEnabled.info": "",
"forms.showDiscordUniqueName": "显示Discord用户名",
"forms.showDiscordUniqueName.info": "是否公开显示您的Discord用户名 ({{discordUniqueName}}) ",
"forms.commissionsOpen": "开放委托",
"forms.commissionsOpen.info": "",
"forms.commissionText": "委托信息",
"forms.commissionText.info": "你的价格、档期或者其他委托相关的信息",
"forms.customName.info": "如果此栏空着将会显示您的Discord昵称\"{{discordName}}\"",
"forms.country.search.placeholder": "",
"forms.favoriteBadges.nonSupporter": "",
"results.title": "所有成绩",
"results.placing": "排名",
"results.team": "队伍",
@ -175,12 +155,6 @@
"results.button.showHighlights": "显示高光成绩",
"results.button.showAll": "显示全部成绩",
"forms.errors.maxWeapons": "已达到武器数量上限",
"forms.errors.invalidCustomUrl.numbers": "自定义URL不能只包含数字",
"forms.errors.invalidCustomUrl.strangeCharacter": "自定义URL不能包含特殊符号",
"forms.errors.invalidCustomUrl.duplicate": "这个自定义URL已被使用",
"forms.errors.invalidSens": "设置体感感度前请先设置摇杆感度",
"forms.info.customUrl": "",
"forms.info.battlefy": "Battlefy用户名会在部分比赛被用于种子排名和验证",
"search.info": "",
"search.noResults": "没有符合 '{{query}}' 的用户",
"search.pleaseLogIn.header": "",

View File

@ -0,0 +1,29 @@
import "dotenv/config";
import { sql } from "~/db/sql";
import { IN_GAME_NAME_REGEXP } from "~/features/user-page/user-page-constants";
import { logger } from "~/utils/logger";
const users = sql
.prepare('SELECT id, "inGameName" FROM "User" WHERE "inGameName" IS NOT NULL')
.all() as { id: number; inGameName: string }[];
const invalidUsers = users.filter(
(user) => !IN_GAME_NAME_REGEXP.test(user.inGameName),
);
logger.info("Invalid in-game names:");
for (const user of invalidUsers) {
logger.info(`- ${user.inGameName} (id: ${user.id})`);
}
const updateStmt = sql.prepare(
'UPDATE "User" SET "inGameName" = NULL WHERE id = @id',
);
for (const user of invalidUsers) {
updateStmt.run({ id: user.id });
}
logger.info(
`Fixed ${invalidUsers.length} invalid in-game names (out of ${users.length} total)`,
);