fix(frontend): localize avatar validation errors

checkAvatarFile returned hard-coded Russian strings, so an English user picking a
bad file got "Подойдёт PNG, JPG или WebP." with no translation. Route both
messages through the account namespace (validation.avatarType / avatarTooBig) the
same way the auth validators take a t function, keeping ru/en key parity enforced
by the satisfies clause.
This commit is contained in:
loki5512344 2026-09-28 13:35:29 +02:00
parent fd0fe59fee
commit 07b3f55bee
Signed by: boba
GPG key ID: 253067914055423B
5 changed files with 18 additions and 7 deletions

View file

@ -1,4 +1,5 @@
import { api } from '../../shared/api/client' import { api } from '../../shared/api/client'
import type { TFunction } from 'i18next'
export type DeviceLink = { id: string; linked_at: string; last_seen: string | null } export type DeviceLink = { id: string; linked_at: string; last_seen: string | null }
@ -6,9 +7,9 @@ const ALLOWED_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp'])
const MAX_BYTES = 5 * 1024 * 1024 const MAX_BYTES = 5 * 1024 * 1024
/** Client-side avatar checks so a bad file never reaches the server. */ /** Client-side avatar checks so a bad file never reaches the server. */
export function checkAvatarFile(file: File): string | undefined { export function checkAvatarFile(file: File, t: TFunction<'account'>): string | undefined {
if (!ALLOWED_TYPES.has(file.type)) return 'Подойдёт PNG, JPG или WebP.' if (!ALLOWED_TYPES.has(file.type)) return t('validation.avatarType')
if (file.size > MAX_BYTES) return 'Файл больше 5 МБ.' if (file.size > MAX_BYTES) return t('validation.avatarTooBig')
return undefined return undefined
} }

View file

@ -19,7 +19,7 @@ export function AvatarUpload() {
const file = e.target.files?.[0] const file = e.target.files?.[0]
e.target.value = '' e.target.value = ''
if (!file) return if (!file) return
const problem = checkAvatarFile(file) const problem = checkAvatarFile(file, t)
if (problem) { if (problem) {
setError(problem) setError(problem)
return return

View file

@ -9,6 +9,10 @@ export const accountEn = {
avatar: { avatar: {
change: 'Change avatar', change: 'Change avatar',
}, },
validation: {
avatarType: 'PNG, JPG or WebP please.',
avatarTooBig: 'The file is over 5 MB.',
},
devices: { devices: {
heading: 'Linked games', heading: 'Linked games',
linkedAt: 'Linked {{date}}', linkedAt: 'Linked {{date}}',

View file

@ -6,6 +6,10 @@ export const accountRu = {
avatar: { avatar: {
change: 'Сменить аватар', change: 'Сменить аватар',
}, },
validation: {
avatarType: 'Подойдёт PNG, JPG или WebP.',
avatarTooBig: 'Файл больше 5 МБ.',
},
devices: { devices: {
heading: 'Привязанные игры', heading: 'Привязанные игры',
linkedAt: 'Привязано {{date}}', linkedAt: 'Привязано {{date}}',

View file

@ -1,5 +1,6 @@
import { screen, within } from '@testing-library/react' import { screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { getI18n } from 'react-i18next'
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import { checkAvatarFile } from '../api' import { checkAvatarFile } from '../api'
import { DeviceList } from '../components/DeviceList' import { DeviceList } from '../components/DeviceList'
@ -7,10 +8,11 @@ import { renderApp } from '../../../test/render'
import { json, mockFetch } from '../../../test/fetch' import { json, mockFetch } from '../../../test/fetch'
test('avatar file checks', () => { test('avatar file checks', () => {
expect(checkAvatarFile(new File(['x'], 'a.gif', { type: 'image/gif' }))).toBe('Подойдёт PNG, JPG или WebP.') const t = getI18n().getFixedT('ru', 'account')
expect(checkAvatarFile(new File(['x'], 'a.gif', { type: 'image/gif' }), t)).toBe('Подойдёт PNG, JPG или WebP.')
const big = new File([new Uint8Array(5 * 1024 * 1024 + 1)], 'a.png', { type: 'image/png' }) const big = new File([new Uint8Array(5 * 1024 * 1024 + 1)], 'a.png', { type: 'image/png' })
expect(checkAvatarFile(big)).toBe('Файл больше 5 МБ.') expect(checkAvatarFile(big, t)).toBe('Файл больше 5 МБ.')
expect(checkAvatarFile(new File(['x'], 'a.png', { type: 'image/png' }))).toBeUndefined() expect(checkAvatarFile(new File(['x'], 'a.png', { type: 'image/png' }), t)).toBeUndefined()
}) })
test('revoking a device asks for confirmation first', async () => { test('revoking a device asks for confirmation first', async () => {