feat(frontend): use plain-language validation messages

This commit is contained in:
Elias Schneider
2026-10-27 23:32:53 +01:00
parent a962c88a5c
commit 23a21c1949
4 changed files with 63 additions and 11 deletions
+11 -1
View File
@@ -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"
}
+7 -9
View File
@@ -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`)
@@ -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;
}
};
+2 -1
View File
@@ -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 }) => {