diff --git a/app/components/IngameNameInput.module.css b/app/components/IngameNameInput.module.css new file mode 100644 index 000000000..e58e3574b --- /dev/null +++ b/app/components/IngameNameInput.module.css @@ -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; + } +} diff --git a/app/components/IngameNameInput.tsx b/app/components/IngameNameInput.tsx new file mode 100644 index 000000000..9098474fa --- /dev/null +++ b/app/components/IngameNameInput.tsx @@ -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(null); + const selectionRef = React.useRef({ start: value.length, end: value.length }); + const pendingCaretRef = React.useRef(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) => { + 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 ( +
+
+ + } + isDisabled={disabled} + aria-label={t("forms:inGameName.addCharacter")} + aria-expanded={isPickerOpen} + onPress={() => setIsPickerOpen((open) => !open)} + /> +
+ {isPickerOpen ? ( + + + {IN_GAME_NAME_CHARACTER_CATEGORIES.map((category) => ( + + {t(category.label)} + + ))} + + {IN_GAME_NAME_CHARACTER_CATEGORIES.map((category) => ( + +
+ {category.characters.map((character) => ( + + ))} +
+
+ ))} +
+ ) : null} +
+ ); +} diff --git a/app/features/api-public/routes/tournament.$id.teams.$teamId.update-member-ign.ts b/app/features/api-public/routes/tournament.$id.teams.$teamId.update-member-ign.ts index ca0a95424..3657219f4 100644 --- a/app/features/api-public/routes/tournament.$id.teams.$teamId.update-member-ign.ts +++ b/app/features/api-public/routes/tournament.$id.teams.$teamId.update-member-ign.ts @@ -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) => { diff --git a/app/features/tournament-admin/tournament-admin-registration-schemas.ts b/app/features/tournament-admin/tournament-admin-registration-schemas.ts index e07e56b9e..f30f0d80e 100644 --- a/app/features/tournament-admin/tournament-admin-registration-schemas.ts +++ b/app/features/tournament-admin/tournament-admin-registration-schemas.ts @@ -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", - }, }), }), }); diff --git a/app/features/user-page/in-game-name.test.ts b/app/features/user-page/in-game-name.test.ts new file mode 100644 index 000000000..f327a1eb1 --- /dev/null +++ b/app/features/user-page/in-game-name.test.ts @@ -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); + } + }); +}); diff --git a/app/features/user-page/in-game-name.ts b/app/features/user-page/in-game-name.ts new file mode 100644 index 000000000..f433403ff --- /dev/null +++ b/app/features/user-page/in-game-name.ts @@ -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; +}>; + +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([ + ...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; +} diff --git a/app/features/user-page/user-page-constants.test.ts b/app/features/user-page/user-page-constants.test.ts deleted file mode 100644 index ed9c25735..000000000 --- a/app/features/user-page/user-page-constants.test.ts +++ /dev/null @@ -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, - ); - } - }); -}); diff --git a/app/features/user-page/user-page-constants.ts b/app/features/user-page/user-page-constants.ts index bb8312ce3..916021bc0 100644 --- a/app/features/user-page/user-page-constants.ts +++ b/app/features/user-page/user-page-constants.ts @@ -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; diff --git a/app/features/user-page/user-page-schemas.ts b/app/features/user-page/user-page-schemas.ts index e7c39382f..6add7f675 100644 --- a/app/features/user-page/user-page-schemas.ts +++ b/app/features/user-page/user-page-schemas.ts @@ -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: [ diff --git a/app/form/FormField.tsx b/app/form/FormField.tsx index 8a2b1a9b9..78df5467f 100644 --- a/app/form/FormField.tsx +++ b/app/form/FormField.tsx @@ -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 ( + void} + /> + ); + } + if (formField.type === "switch") { return ( >( 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< diff --git a/app/form/fields/InGameNameFormField.tsx b/app/form/fields/InGameNameFormField.tsx new file mode 100644 index 000000000..d4680a78e --- /dev/null +++ b/app/form/fields/InGameNameFormField.tsx @@ -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 ( + + onBlur?.()} + disabled={disabled} + {...ariaAttributes({ id, bottomText, error, required })} + /> + + ); +} diff --git a/app/form/types.ts b/app/form/types.ts index be51e88ed..c86a01be3 100644 --- a/app/form/types.ts +++ b/app/form/types.ts @@ -45,6 +45,11 @@ interface FormFieldTextarea extends FormFieldBase { maxLength: number; } +interface FormFieldInGameName extends FormFieldBase { + maxLength: number; + required: boolean; +} + interface FormFieldItem { label: string | number | ((lang: string) => string); value: V; @@ -181,6 +186,7 @@ interface FormFieldWeaponSelect extends FormFieldBase { export type FormField = | FormFieldBase<"custom"> | FormFieldText<"text-field"> + | FormFieldInGameName<"in-game-name"> | FormFieldTextarea<"text-area"> | FormFieldBase<"switch"> | FormFieldSelect<"select", V> diff --git a/locales/da/forms.json b/locales/da/forms.json index 84fb5f477..e69f9028c 100644 --- a/locales/da/forms.json +++ b/locales/da/forms.json @@ -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": "", diff --git a/locales/de/forms.json b/locales/de/forms.json index 9cc811d4e..89220859c 100644 --- a/locales/de/forms.json +++ b/locales/de/forms.json @@ -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": "", diff --git a/locales/en/forms.json b/locales/en/forms.json index c10e781b1..5d613694f 100644 --- a/locales/en/forms.json +++ b/locales/en/forms.json @@ -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", diff --git a/locales/es-ES/forms.json b/locales/es-ES/forms.json index 1221ec9b9..fcba8079e 100644 --- a/locales/es-ES/forms.json +++ b/locales/es-ES/forms.json @@ -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": "", diff --git a/locales/es-US/forms.json b/locales/es-US/forms.json index e08a2e0f2..3f7693d09 100644 --- a/locales/es-US/forms.json +++ b/locales/es-US/forms.json @@ -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": "", diff --git a/locales/fr-CA/forms.json b/locales/fr-CA/forms.json index 4a80aa433..8ae17aa56 100644 --- a/locales/fr-CA/forms.json +++ b/locales/fr-CA/forms.json @@ -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": "", diff --git a/locales/fr-EU/forms.json b/locales/fr-EU/forms.json index 7c8423edf..1c61bf359 100644 --- a/locales/fr-EU/forms.json +++ b/locales/fr-EU/forms.json @@ -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": "", diff --git a/locales/he/forms.json b/locales/he/forms.json index 690637309..d19fc5536 100644 --- a/locales/he/forms.json +++ b/locales/he/forms.json @@ -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": "", diff --git a/locales/it/forms.json b/locales/it/forms.json index 5a3d216f0..1097000b1 100644 --- a/locales/it/forms.json +++ b/locales/it/forms.json @@ -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": "", diff --git a/locales/ja/forms.json b/locales/ja/forms.json index fdafe8228..c3cc9328a 100644 --- a/locales/ja/forms.json +++ b/locales/ja/forms.json @@ -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": "", diff --git a/locales/ko/forms.json b/locales/ko/forms.json index b856f8858..a8be337a7 100644 --- a/locales/ko/forms.json +++ b/locales/ko/forms.json @@ -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": "", diff --git a/locales/nl/forms.json b/locales/nl/forms.json index b0de304cb..99c48d06b 100644 --- a/locales/nl/forms.json +++ b/locales/nl/forms.json @@ -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": "", diff --git a/locales/pl/forms.json b/locales/pl/forms.json index a0f9e012b..0904b9cf9 100644 --- a/locales/pl/forms.json +++ b/locales/pl/forms.json @@ -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": "", diff --git a/locales/pt-BR/forms.json b/locales/pt-BR/forms.json index 28b6fc315..8be814f3c 100644 --- a/locales/pt-BR/forms.json +++ b/locales/pt-BR/forms.json @@ -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": "", diff --git a/locales/ru/forms.json b/locales/ru/forms.json index c8c57675b..d49559d4f 100644 --- a/locales/ru/forms.json +++ b/locales/ru/forms.json @@ -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": "", diff --git a/locales/zh/forms.json b/locales/zh/forms.json index e09a546ee..1e9b20d29 100644 --- a/locales/zh/forms.json +++ b/locales/zh/forms.json @@ -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": "",