Ingame name input (#3163)

This commit is contained in:
hfcRed
2026-06-16 14:55:43 +02:00
committed by GitHub
parent 2732f55359
commit a5b3ed2e51
29 changed files with 602 additions and 67 deletions

View File

@@ -0,0 +1,59 @@
.root {
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.inputRow {
position: relative;
display: flex;
align-items: center;
& input {
padding-right: calc(var(--field-size) + var(--s-1));
}
& button {
position: absolute;
right: var(--s-1-5);
}
}
.picker {
border: var(--border-style);
border-radius: var(--radius-field);
background-color: var(--color-bg-high);
gap: var(--s-1);
padding-top: var(--s-0-5);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(2.25rem, 1fr));
gap: var(--s-1);
max-height: 300px;
overflow-y: auto;
padding: var(--s-2);
}
.glyph {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
border: var(--border-style);
border-radius: var(--radius-selector);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-sm);
cursor: pointer;
&:hover {
background-color: var(--color-bg-higher);
}
&:focus-visible {
outline: var(--focus-ring);
outline-offset: 1px;
}
}

View File

@@ -0,0 +1,165 @@
import clsx from "clsx";
import { Languages } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { SendouButton } from "~/components/elements/Button";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import {
IN_GAME_NAME_CHARACTER_CATEGORIES,
IN_GAME_NAME_MAX_LENGTH,
inGameNameLength,
sanitizeInGameName,
} from "~/features/user-page/in-game-name";
import styles from "./IngameNameInput.module.css";
interface IngameNameInputProps
extends Pick<
React.AriaAttributes,
"aria-invalid" | "aria-describedby" | "aria-errormessage" | "aria-required"
> {
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
id?: string;
name?: string;
disabled?: boolean;
placeholder?: string;
}
export function IngameNameInput({
value,
onChange,
onBlur,
id,
name,
disabled,
placeholder,
...ariaProps
}: IngameNameInputProps) {
const { t } = useTranslation(["forms"]);
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const selectionRef = React.useRef({ start: value.length, end: value.length });
const pendingCaretRef = React.useRef<number | null>(null);
React.useEffect(() => {
if (pendingCaretRef.current === null) return;
const caret = pendingCaretRef.current;
pendingCaretRef.current = null;
const input = inputRef.current;
input?.focus();
input?.setSelectionRange(caret, caret);
});
const rememberSelection = () => {
const input = inputRef.current;
if (!input) return;
selectionRef.current = {
start: input.selectionStart ?? value.length,
end: input.selectionEnd ?? value.length,
};
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const raw = event.target.value;
const cleaned = sanitizeInGameName(raw);
if (cleaned !== raw) {
const caret = event.target.selectionStart ?? cleaned.length;
pendingCaretRef.current = Math.max(
0,
Math.min(cleaned.length, caret - (raw.length - cleaned.length)),
);
}
onChange(cleaned);
};
const insertCharacter = (character: string) => {
const { start, end } = selectionRef.current;
const before = value.slice(0, start);
const after = value.slice(end);
if (
inGameNameLength(before + after) + inGameNameLength(character) >
IN_GAME_NAME_MAX_LENGTH
) {
return;
}
const caret = before.length + character.length;
pendingCaretRef.current = caret;
selectionRef.current = { start: caret, end: caret };
onChange(before + character + after);
};
return (
<div className={styles.root}>
<div className={styles.inputRow}>
<input
ref={inputRef}
id={id}
name={name}
type="text"
value={value}
disabled={disabled}
placeholder={placeholder}
maxLength={IN_GAME_NAME_MAX_LENGTH}
autoComplete="off"
spellCheck={false}
onChange={handleChange}
onBlur={onBlur}
onSelect={rememberSelection}
{...ariaProps}
/>
<SendouButton
variant="minimal"
size="small"
shape="square"
icon={<Languages />}
isDisabled={disabled}
aria-label={t("forms:inGameName.addCharacter")}
aria-expanded={isPickerOpen}
onPress={() => setIsPickerOpen((open) => !open)}
/>
</div>
{isPickerOpen ? (
<SendouTabs className={styles.picker}>
<SendouTabList>
{IN_GAME_NAME_CHARACTER_CATEGORIES.map((category) => (
<SendouTab key={category.id} id={category.id}>
{t(category.label)}
</SendouTab>
))}
</SendouTabList>
{IN_GAME_NAME_CHARACTER_CATEGORIES.map((category) => (
<SendouTabPanel key={category.id} id={category.id}>
<div className={clsx(styles.grid, "scrollbar")}>
{category.characters.map((character) => (
<button
key={character}
type="button"
className={styles.glyph}
onMouseDown={(event) => event.preventDefault()}
onClick={() => insertCharacter(character)}
>
{character}
</button>
))}
</div>
</SendouTabPanel>
))}
</SendouTabs>
) : null}
</div>
);
}

View File

@@ -6,7 +6,7 @@ import {
clearTournamentDataCache,
tournamentFromDB,
} from "~/features/tournament-bracket/core/Tournament.server";
import { IN_GAME_NAME_REGEXP } from "~/features/user-page/user-page-constants";
import { inGameNameIsValid } from "~/features/user-page/in-game-name";
import {
badRequestIfFalsy,
errorToastIfFalsy,
@@ -23,7 +23,7 @@ const paramsSchema = z.object({
const bodySchema = z.object({
userId: id,
inGameName: z.string().regex(IN_GAME_NAME_REGEXP),
inGameName: z.string().refine(inGameNameIsValid),
});
export const action = async (args: ActionFunctionArgs) => {

View File

@@ -5,6 +5,7 @@ import {
fieldset,
idConstantOptional,
image,
inGameName,
selectDynamic,
stringConstant,
teamSearchOptional,
@@ -14,21 +15,12 @@ import {
userSearch,
} from "~/form/fields";
import { TEAM } from "../team/team-constants";
import { IN_GAME_NAME_REGEXP } from "../user-page/user-page-constants";
/** Combined in-game name e.g. `Sendou#1234` is at most 10 + `#` + 5 characters. */
const IN_GAME_NAME_MAX_LENGTH = 16;
const memberFieldset = fieldset({
fields: z.object({
userId: userSearch({ label: "labels.player" }),
inGameName: textFieldOptional({
inGameName: inGameName({
label: "labels.inGameName",
maxLength: IN_GAME_NAME_MAX_LENGTH,
regExp: {
pattern: IN_GAME_NAME_REGEXP,
message: "forms:errors.profileInGameName",
},
}),
}),
});

View File

@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import { inGameNameIsValid } from "./in-game-name";
describe("inGameNameIsValid", () => {
it("should pass valid in-game names", () => {
const validNames = [
"Sendou#12345",
"The Player#12345",
" a#1234",
"A#1234",
"Player#abcd",
"Café#1234",
"Ελλαδα#1234",
"テストab#1234",
"★Test★#1234",
"½#1234",
"naïve#1234",
];
for (const name of validNames) {
expect(inGameNameIsValid(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",
"𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔#1234",
"名前テスト1234#ab12c",
"☆CR☆Sh𝓔𝓔p!#1234",
"日本語#1234",
];
for (const name of invalidNames) {
expect(inGameNameIsValid(name), `expected "${name}" to fail`).toBe(false);
}
});
it("should reject characters the Switch keyboard does not allow in names", () => {
const invalidNames = [
"test@me#1234",
"100%#1234",
"a\\b#1234",
"●#1234",
"♥#1234",
];
for (const name of invalidNames) {
expect(inGameNameIsValid(name), `expected "${name}" to fail`).toBe(false);
}
});
});

View File

@@ -0,0 +1,107 @@
import type { FormsTranslationKey } from "~/form/types";
const IN_GAME_NAME = {
NAME_MAX_LENGTH: 10,
DISCRIMINATOR_MIN_LENGTH: 4,
DISCRIMINATOR_MAX_LENGTH: 5,
};
export const IN_GAME_NAME_MAX_LENGTH =
IN_GAME_NAME.NAME_MAX_LENGTH + 1 + IN_GAME_NAME.DISCRIMINATOR_MAX_LENGTH;
/**
* @see {@link https://github.com/kjhf/NintendoSwitchKeyboard}
*/
export const IN_GAME_NAME_CHARACTER_CATEGORIES = [
{
id: "symbols",
label: "inGameName.categories.symbols",
characters: [
..."¿¡′‘’‚‛•…″“”„«»←→↑↓⇒⇔˜ˊˋ¢€£¥¤𝑓×÷±∞√¬∀⊂⊃∴∵⁀∂№°¹²³¼½¾♪♭♀♂⚪⚫◎◻◼◇◆△▲▽▼☆★©®™§¶†⍑",
],
},
{
id: "accented",
label: "inGameName.categories.accented",
characters: [
..."àáâãäåæāăąçćċčðďdždzèéêëēęěğġģħìíîïīįıijķĺļľłÀÁÂÃÄÅÆĀĂĄÇĆĊČÐĎDžDzÈÉÊËĒĘĚĞĠĢĦÌÍÎÏĪĮİIJĶĹĻĽŁñńņňòóôõöøœőŕřšßśşþťţùúûüūůűųýÿźżžÑŃŅŇÒÓÔÕÖØŒŐŔŘŠẞŚŞÞŤŢÙÚÛÜŪŮŰŲÝŸŹŻŽ",
],
},
{
id: "greek",
label: "inGameName.categories.greek",
characters: [..."αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ"],
},
{
id: "cyrillic",
label: "inGameName.categories.cyrillic",
characters: range(0x0410, 0x044f),
},
{
id: "hiragana",
label: "inGameName.categories.hiragana",
characters: range(0x3041, 0x3096),
},
{
id: "katakana",
label: "inGameName.categories.katakana",
characters: range(0x30a1, 0x30fa),
},
] as const satisfies ReadonlyArray<{
id: string;
label: FormsTranslationKey;
characters: ReadonlyArray<string>;
}>;
const SPECIAL_CHARACTERS = IN_GAME_NAME_CHARACTER_CATEGORIES.flatMap(
(category) => category.characters,
);
const ASCII_NOT_VALID = new Set(["%", "@", "\\"]);
const ASCII_CHARACTERS = range(0x20, 0x7e).filter(
(character) => !ASCII_NOT_VALID.has(character),
);
const ALLOWED_CHARACTERS = new Set<string>([
...ASCII_CHARACTERS,
...SPECIAL_CHARACTERS,
]);
const IN_GAME_NAME_REGEXP = new RegExp(
`^(.+)#([0-9a-z]{${IN_GAME_NAME.DISCRIMINATOR_MIN_LENGTH},${IN_GAME_NAME.DISCRIMINATOR_MAX_LENGTH}})$`,
"u",
);
/** Length of a string counted in code points (so astral characters count as one). */
export function inGameNameLength(value: string): number {
return [...value].length;
}
export function sanitizeInGameName(value: string): string {
return [...value.normalize("NFC")]
.filter((character) => ALLOWED_CHARACTERS.has(character))
.join("");
}
export function inGameNameIsValid(value: string): boolean {
const match = IN_GAME_NAME_REGEXP.exec(value);
if (!match) return false;
const nameCharacters = [...match[1]];
if (
nameCharacters.length < 1 ||
nameCharacters.length > IN_GAME_NAME.NAME_MAX_LENGTH
) {
return false;
}
return nameCharacters.every((character) => ALLOWED_CHARACTERS.has(character));
}
function range(from: number, to: number): string[] {
const characters: string[] = [];
for (let codePoint = from; codePoint <= to; codePoint++) {
characters.push(String.fromCodePoint(codePoint));
}
return characters;
}

View File

@@ -1,41 +0,0 @@
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",
"☆CR☆Sh𝓔𝓔p!#1234",
];
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",
"𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔𝓔#1234",
];
for (const name of invalidNames) {
expect(IN_GAME_NAME_REGEXP.test(name), `expected "${name}" to fail`).toBe(
false,
);
}
});
});

View File

@@ -7,8 +7,6 @@ export const USER = {
CUSTOM_URL_MAX_LENGTH: 32,
CUSTOM_NAME_MAX_LENGTH: 32,
BATTLEFY_MAX_LENGTH: 32,
IN_GAME_NAME_TEXT_MAX_LENGTH: 20,
IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH: 5,
WEAPON_POOL_MAX_SIZE: 5,
COMMISSION_TEXT_MAX_LENGTH: 1000,
MOD_NOTE_MAX_LENGTH: 2000,
@@ -20,8 +18,6 @@ export const USER = {
export const SPL2_JOIN_ORDER_CUTOFF = 13_589;
export const IN_GAME_NAME_REGEXP = /^.{1,10}#[0-9a-z]{4,5}$/u;
export const MATCHES_PER_SEASONS_PAGE = 8;
export const RESULTS_PER_PAGE = 25;
export const HIGHLIGHTS_RESULTS_MAX = 500;

View File

@@ -9,6 +9,7 @@ import {
dualSelectOptional,
idConstantOptional,
image,
inGameName,
selectDynamicOptional,
stringConstant,
textAreaOptional,
@@ -38,7 +39,6 @@ import { allWidgetsFlat, findWidgetById } from "./core/widgets/portfolio";
import {
HIGHLIGHT_CHECKBOX_NAME,
HIGHLIGHT_TOURNAMENT_CHECKBOX_NAME,
IN_GAME_NAME_REGEXP,
USER,
} from "./user-page-constants";
@@ -87,17 +87,9 @@ export const userEditProfileBaseSchema = z.object({
message: "forms:errors.profileCustomUrlNumbers",
},
}),
inGameName: textFieldOptional({
inGameName: inGameName({
label: "labels.inGameName",
bottomText: "bottomTexts.profileInGameName",
maxLength:
USER.IN_GAME_NAME_TEXT_MAX_LENGTH +
1 +
USER.IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH,
regExp: {
pattern: IN_GAME_NAME_REGEXP,
message: "forms:errors.profileInGameName",
},
}),
sensitivity: dualSelectOptional({
fields: [

View File

@@ -8,6 +8,7 @@ import { DatetimeFormField } from "./fields/DatetimeFormField";
import { DualSelectFormField } from "./fields/DualSelectFormField";
import { FieldsetFormField } from "./fields/FieldsetFormField";
import { ImageFormField } from "./fields/ImageFormField";
import { InGameNameFormField } from "./fields/InGameNameFormField";
import { InputFormField } from "./fields/InputFormField";
import {
CheckboxGroupFormField,
@@ -189,6 +190,18 @@ export function FormField({
);
}
if (formField.type === "in-game-name") {
return (
<InGameNameFormField
{...commonProps}
{...formField}
disabled={disabled}
value={value as string}
onChange={handleChange as (v: string) => void}
/>
);
}
if (formField.type === "switch") {
return (
<SwitchFormField

View File

@@ -1,5 +1,9 @@
import * as R from "remeda";
import { z } from "zod";
import {
IN_GAME_NAME_MAX_LENGTH,
inGameNameIsValid,
} from "~/features/user-page/in-game-name";
import { canonicalWeaponSplId } from "~/modules/in-game-lists/weapon-ids";
import {
date,
@@ -199,6 +203,29 @@ function textFieldRefined<T extends z.ZodType<string | null>>(
return result as T;
}
export function inGameName(
args: WithTypedTranslationKeys<{
label?: FormsTranslationKey;
bottomText?: FormsTranslationKey;
}>,
) {
const schema = safeNullableStringSchema({
max: IN_GAME_NAME_MAX_LENGTH,
}).refine((val) => val === null || inGameNameIsValid(val), {
message: "forms:errors.profileInGameName",
});
return schema.register(formRegistry, {
...args,
label: prefixKey(args.label),
bottomText: prefixKey(args.bottomText),
maxLength: IN_GAME_NAME_MAX_LENGTH,
required: false,
type: "in-game-name",
initialValue: "",
});
}
export function numberField(
args: WithTypedTranslationKeys<
Omit<

View File

@@ -0,0 +1,49 @@
import * as React from "react";
import { IngameNameInput } from "~/components/IngameNameInput";
import { inGameNameLength } from "~/features/user-page/in-game-name";
import type { FormFieldProps } from "../types";
import { ariaAttributes } from "../utils";
import { FormFieldWrapper } from "./FormFieldWrapper";
type InGameNameFormFieldProps = FormFieldProps<"in-game-name"> & {
disabled?: boolean;
value: string;
onChange: (value: string) => void;
};
export function InGameNameFormField({
name,
label,
bottomText,
maxLength,
error,
onBlur,
required,
disabled,
value,
onChange,
}: InGameNameFormFieldProps) {
const id = React.useId();
return (
<FormFieldWrapper
id={id}
name={name}
label={label}
required={required}
error={error}
bottomText={bottomText}
valueLimits={{ current: inGameNameLength(value), max: maxLength }}
>
<IngameNameInput
id={id}
name={name}
value={value}
onChange={onChange}
onBlur={() => onBlur?.()}
disabled={disabled}
{...ariaAttributes({ id, bottomText, error, required })}
/>
</FormFieldWrapper>
);
}

View File

@@ -45,6 +45,11 @@ interface FormFieldTextarea<T extends string> extends FormFieldBase<T> {
maxLength: number;
}
interface FormFieldInGameName<T extends string> extends FormFieldBase<T> {
maxLength: number;
required: boolean;
}
interface FormFieldItem<V extends string> {
label: string | number | ((lang: string) => string);
value: V;
@@ -181,6 +186,7 @@ interface FormFieldWeaponSelect<T extends string> extends FormFieldBase<T> {
export type FormField<V extends string = string> =
| FormFieldBase<"custom">
| FormFieldText<"text-field">
| FormFieldInGameName<"in-game-name">
| FormFieldTextarea<"text-area">
| FormFieldBase<"switch">
| FormFieldSelect<"select", V>

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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)",
"inGameName.addCharacter": "Add special character",
"inGameName.categories.symbols": "Symbols",
"inGameName.categories.accented": "Accented",
"inGameName.categories.greek": "Greek",
"inGameName.categories.cyrillic": "Cyrillic",
"inGameName.categories.hiragana": "Hiragana",
"inGameName.categories.katakana": "Katakana",
"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

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "Alguien ya tiene ese enlace personalizado",
"errors.profileSensBothOrNeither": "Motion sens can't be set if R-stick sens isn't",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "Alguien ya tiene ese enlace personalizado",
"errors.profileSensBothOrNeither": "Sens de giroscopio no se poner sin la sens de palanca",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "מישהו כבר משתמש בכתובת URL המותאמת אישית הזו",
"errors.profileSensBothOrNeither": "לא ניתן להגדיר את רגישות התנועה אם רגישות הסטיק לא מוגדרת",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "このカスタム URL はすでに使用されています",
"errors.profileSensBothOrNeither": "右スティックの感度が設定されていない場合、感度を設定することはできません",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "",
"errors.profileSensBothOrNeither": "",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"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": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "Кто-то уже использует этот пользовательский URL",
"errors.profileSensBothOrNeither": "Чувствительность наклона не может быть указана, если не указана чувствительность стика",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",

View File

@@ -264,6 +264,13 @@
"errors.profileCustomUrlDuplicate": "这个自定义URL已被使用",
"errors.profileSensBothOrNeither": "设置体感感度前请先设置摇杆感度",
"errors.profileInGameName": "",
"inGameName.addCharacter": "",
"inGameName.categories.symbols": "",
"inGameName.categories.accented": "",
"inGameName.categories.greek": "",
"inGameName.categories.cyrillic": "",
"inGameName.categories.hiragana": "",
"inGameName.categories.katakana": "",
"labels.pronoun": "",
"bottomTexts.profilePronouns": "",
"errors.profilePronounsBothOrNeither": "",