diff --git a/frontend/messages/en.json b/frontend/messages/en.json index d4578c0d..55c39e35 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1101,5 +1101,15 @@ "register_your_first_api": "Register your first API", "create_your_first_api_key": "Create your first API key", "add_your_first_group": "Add your first group", - "add_your_first_oidc_client": "Add your first OIDC client" + "add_your_first_oidc_client": "Add your first OIDC client", + "validation_required": "Required", + "validation_min_length": "Enter at least {count} characters", + "validation_max_length": "Enter at most {count} characters", + "validation_min_number": "Enter {minimum} or more", + "validation_max_number": "Enter {maximum} or less", + "validation_min_items": "Choose at least {count}", + "validation_max_items": "Choose at most {count}", + "validation_email": "Enter a valid email address", + "validation_url": "Enter a valid URL", + "validation_number": "Enter a number" } diff --git a/frontend/src/lib/utils/locale.util.ts b/frontend/src/lib/utils/locale.util.ts index adc16bb0..7e73411c 100644 --- a/frontend/src/lib/utils/locale.util.ts +++ b/frontend/src/lib/utils/locale.util.ts @@ -1,25 +1,23 @@ -import { - extractLocaleFromCookie, - setLocale as setParaglideLocale, - type Locale -} from '#lib/paraglide/runtime.js'; +import { getLocale, setLocale as setParaglideLocale, type Locale } from '#lib/paraglide/runtime.js'; import { setDefaultOptions } from 'date-fns'; import { z } from 'zod/v4'; +import { validationErrorMap } from './validation-error-util'; export async function setLocale(locale: Locale, reload = true) { await setLocaleForLibraries(locale); setParaglideLocale(locale, { reload }); - document.documentElement.lang = locale; } -export async function setLocaleForLibraries( - locale: Locale = (extractLocaleFromCookie() as Locale) || 'en' -) { +export async function setLocaleForLibraries(locale: Locale = getLocale()) { + document.documentElement.lang = locale; + let dateFnsLocale: string = locale; if (dateFnsLocale === 'en') { dateFnsLocale = 'en-US'; // datefns doesn't have 'en' } + z.config({ customError: validationErrorMap }); + const [zodResult, dateFnsResult] = await Promise.allSettled([ import(`../../../node_modules/zod/v4/locales/${locale}.js`), import(`../../../node_modules/date-fns/locale/${dateFnsLocale}.js`) diff --git a/frontend/src/lib/utils/validation-error-util.ts b/frontend/src/lib/utils/validation-error-util.ts new file mode 100644 index 00000000..fd0ecb4f --- /dev/null +++ b/frontend/src/lib/utils/validation-error-util.ts @@ -0,0 +1,43 @@ +import { m } from '#lib/paraglide/messages.js'; +import type { z } from 'zod/v4'; + +const isEmpty = (input: unknown) => input === undefined || input === null || input === ''; + +// Plain-language messages for the checks forms use; anything not covered falls back to Zod's own locale +export const validationErrorMap: z.core.$ZodErrorMap = (issue) => { + switch (issue.code) { + case 'invalid_type': + if (isEmpty(issue.input)) return m.validation_required(); + if (issue.expected === 'number' || issue.expected === 'int') return m.validation_number(); + return undefined; + case 'too_small': { + const count = Number(issue.minimum); + if (issue.origin === 'string') { + return count <= 1 ? m.validation_required() : m.validation_min_length({ count }); + } + if (issue.origin === 'number' || issue.origin === 'int') { + return m.validation_min_number({ minimum: count }); + } + if (issue.origin === 'array' || issue.origin === 'set') + return m.validation_min_items({ count }); + return undefined; + } + case 'too_big': { + const count = Number(issue.maximum); + if (issue.origin === 'string') return m.validation_max_length({ count }); + if (issue.origin === 'number' || issue.origin === 'int') { + return m.validation_max_number({ maximum: count }); + } + if (issue.origin === 'array' || issue.origin === 'set') + return m.validation_max_items({ count }); + return undefined; + } + case 'invalid_format': + if (isEmpty(issue.input)) return m.validation_required(); + if (issue.format === 'email') return m.validation_email(); + if (issue.format === 'url') return m.validation_url(); + return undefined; + default: + return undefined; + } +}; diff --git a/tests/specs/user-signup.spec.ts b/tests/specs/user-signup.spec.ts index 053495c2..111fafc8 100644 --- a/tests/specs/user-signup.spec.ts +++ b/tests/specs/user-signup.spec.ts @@ -208,9 +208,10 @@ test.describe('User Signup', () => { await page.goto('/signup'); + await page.getByLabel('Email').fill('not-an-email'); await page.getByRole('button', { name: 'Create account' }).click(); - await expect(page.getByText('Invalid email address').first()).toBeVisible(); + await expect(page.getByText('Enter a valid email address')).toBeVisible(); }); test('Open signup - duplicate email shows error', async ({ page }) => {