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:
parent
fd0fe59fee
commit
07b3f55bee
5 changed files with 18 additions and 7 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}}',
|
||||||
|
|
|
||||||
|
|
@ -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}}',
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue