Skip to content

Commit

Permalink
🌐 [open-formulieren/open-forms#3607] Localize error messages
Browse files Browse the repository at this point in the history
Ensure the validation error messages are translated and sufficiently
accessible for people using screen readers (meaning the field label
must be mentioned in the validation error message).
  • Loading branch information
sergei-maertens committed Jan 9, 2024
1 parent 8a4a36e commit f152a1f
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 82 deletions.
84 changes: 59 additions & 25 deletions src/formio/components/AddressNL.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import debounce from 'lodash/debounce';
import React, {useEffect} from 'react';
import {createRoot} from 'react-dom/client';
import {Formio} from 'react-formio';
import {FormattedMessage, IntlProvider, createIntl} from 'react-intl';
import {FormattedMessage, IntlProvider, defineMessages, useIntl} from 'react-intl';
import {z} from 'zod';
import {toFormikValidationSchema} from 'zod-formik-adapter';

Expand Down Expand Up @@ -136,7 +136,6 @@ export default class AddressNL extends Field {

renderReact() {
const required = this.component?.validate?.required || false;
const intl = createIntl(this.options.intl);
const initialValues = {...this.emptyValue, ...this.dataValue};

this.reactRoot.render(
Expand All @@ -147,16 +146,11 @@ export default class AddressNL extends Field {
requiredFieldsWithAsterisk: this.options.evalContext.requiredFieldsWithAsterisk,
}}
>
<Formik
<AddressNLForm
initialValues={initialValues}
initialTouched={{
postcode: true,
houseNumber: true,
}}
validationSchema={toFormikValidationSchema(addressNLSchema(required, intl))}
>
<FormikAddress required={required} setFormioValues={this.onFormikChange.bind(this)} />
</Formik>
required={required}
setFormioValues={this.onFormikChange.bind(this)}
/>
</ConfigContext.Provider>
</IntlProvider>
);
Expand All @@ -170,6 +164,17 @@ export default class AddressNL extends Field {
}
}

const FIELD_LABELS = defineMessages({
postcode: {
description: 'Label for addressNL postcode input',
defaultMessage: 'Postcode',
},
houseNumber: {
description: 'Label for addressNL houseNumber input',
defaultMessage: 'House number',
},
});

const addressNLSchema = (required, intl) => {
let postcodeSchema = z.string().regex(/^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$/);
let houseNumberSchema = z.string().regex(/^\d{1,5}$/);
Expand Down Expand Up @@ -197,7 +202,7 @@ const addressNLSchema = (required, intl) => {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: intl.formatMessage({
descripion:
description:
'ZOD error message when AddressNL postcode is provided but not houseNumber',
defaultMessage: 'You must provide a house number.',
}),
Expand All @@ -209,7 +214,7 @@ const addressNLSchema = (required, intl) => {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: intl.formatMessage({
descripion:
description:
'ZOD error message when AddressNL houseNumber is provided but not postcode',
defaultMessage: 'You must provide a postcode.',
}),
Expand All @@ -220,6 +225,45 @@ const addressNLSchema = (required, intl) => {
});
};

const AddressNLForm = ({initialValues, required, setFormioValues}) => {
const intl = useIntl();

const errorMap = (issue, ctx) => {
switch (issue.code) {
case z.ZodIssueCode.invalid_type: {
if (issue.received === z.ZodParsedType.undefined) {
const fieldName = issue.path.join('.');
const fieldLabel = intl.formatMessage(FIELD_LABELS[fieldName]);
const message = intl.formatMessage(
{
description: 'Required field error message',
defaultMessage: '{field} is required.',
},
{
field: fieldLabel,
}
);
return {message};
}
}
}
return {message: ctx.defaultError}; // use global schema as fallback
};

return (
<Formik
initialValues={initialValues}
initialTouched={{
postcode: true,
houseNumber: true,
}}
validationSchema={toFormikValidationSchema(addressNLSchema(required, intl), {errorMap})}
>
<FormikAddress required={required} setFormioValues={setFormioValues} />
</Formik>
);
};

const FormikAddress = ({required, setFormioValues}) => {
const {values, isValid} = useFormikContext();

Expand All @@ -242,12 +286,7 @@ const FormikAddress = ({required, setFormioValues}) => {
<div className="column column--span-6 openforms-form-field-container">
<TextField
name="houseNumber"
label={
<FormattedMessage
description="Label for addressNL houseNumber input"
defaultMessage="House number"
/>
}
label={<FormattedMessage {...FIELD_LABELS.houseNumber} />}
placeholder="123"
isRequired={required}
/>
Expand Down Expand Up @@ -299,12 +338,7 @@ const PostCodeField = ({required}) => {
return (
<TextField
name="postcode"
label={
<FormattedMessage
description="Label for addressNL postcode input"
defaultMessage="Postcode"
/>
}
label={<FormattedMessage {...FIELD_LABELS.postcode} />}
placeholder="1234 AB"
isRequired={required}
onBlur={onBlur}
Expand Down
22 changes: 11 additions & 11 deletions src/formio/components/AddressNL.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export const ClientSideValidation = {
const canvas = within(canvasElement);

const postcodeInput = await canvas.findByLabelText('Postcode');
const houseNumberInput = await canvas.findByLabelText('Huis nummer');
const houseLetter = await canvas.findByLabelText('Huis letter');
const houseNumberAddition = await canvas.findByLabelText('Huis nummer toevoeging');
const houseNumberInput = await canvas.findByLabelText('Huisnummer');
const houseLetter = await canvas.findByLabelText('Huisletter');
const houseNumberAddition = await canvas.findByLabelText('Huisnummertoevoeging');

await step('Fill only postcode - client side validation error', async () => {
userEvent.type(postcodeInput, '1234AB');
expect(await canvas.findByText('Required')).toBeVisible();
expect(await canvas.findByText('Huisnummer is verplicht.')).toBeVisible();
});

await step('Fill house number field', async () => {
Expand All @@ -61,13 +61,13 @@ export const ClientSideValidation = {

await waitFor(() => {
expect(houseNumberAddition).not.toHaveFocus();
expect(canvas.queryByText('Required')).not.toBeInTheDocument();
expect(canvas.queryByText('/is verplicht/')).not.toBeInTheDocument();
});
});

await step('Clear postcode field, keep house number field', async () => {
userEvent.clear(postcodeInput);
expect(await canvas.findByText('Required')).toBeVisible();
expect(await canvas.findByText('Postcode is verplicht.')).toBeVisible();
});
},
};
Expand All @@ -85,17 +85,17 @@ export const NotRequired = {
const canvas = within(canvasElement);

const postcodeInput = await canvas.findByLabelText('Postcode');
const houseNumberInput = await canvas.findByLabelText('Huis nummer');
const houseNumberInput = await canvas.findByLabelText('Huisnummer');

await step('Enter only postcode, without house number', async () => {
userEvent.type(postcodeInput, '1234AB');
expect(await canvas.findByText('You must provide a house number.')).toBeVisible();
expect(await canvas.findByText('Huisnummer is verplicht.')).toBeVisible();
});

await step('Enter only house number, without postcode', async () => {
userEvent.clear(postcodeInput);
userEvent.type(houseNumberInput, '1');
expect(await canvas.findByText('You must provide a postcode.')).toBeVisible();
expect(await canvas.findByText('Postcode is verplicht.')).toBeVisible();
});
},
};
Expand Down Expand Up @@ -126,7 +126,7 @@ export const WithPassingBRKValidation = {
const postcodeInput = await canvas.findByLabelText('Postcode');
userEvent.type(postcodeInput, '1234AB');

const houseNumberInput = await canvas.findByLabelText('Huis nummer');
const houseNumberInput = await canvas.findByLabelText('Huisnummer');
userEvent.type(houseNumberInput, '1');

// this assertion is not worth much due to the async nature of the validators...
Expand Down Expand Up @@ -166,7 +166,7 @@ export const WithFailedBRKValidation = {
// expect(postcodeInput).toHaveDisplayValue('1234AB');
// });

// const houseNumberInput = await canvas.findByLabelText('Huis nummer');
// const houseNumberInput = await canvas.findByLabelText('Huisnummer');
// userEvent.type(houseNumberInput, '1');
// await waitFor(() => {
// expect(houseNumberInput).toHaveDisplayValue('1');
Expand Down
34 changes: 22 additions & 12 deletions src/i18n/compiled/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,16 @@
"value": "Please accept the privacy policy before submitting"
}
],
"8cxbC6": [
{
"type": 1,
"value": "field"
},
{
"type": 0,
"value": " is required."
}
],
"8uBESg": [
{
"type": 0,
Expand Down Expand Up @@ -487,6 +497,12 @@
"value": "Contact details"
}
],
"GbPef0": [
{
"type": 0,
"value": "You must provide a house number."
}
],
"GiRKAS": [
{
"type": 0,
Expand Down Expand Up @@ -1193,12 +1209,6 @@
"value": "House letter"
}
],
"cHn60V": [
{
"type": 0,
"value": "You must provide a house number."
}
],
"cKFCTI": [
{
"type": 0,
Expand Down Expand Up @@ -1487,6 +1497,12 @@
"value": "Product"
}
],
"lKBAu3": [
{
"type": 0,
"value": "You must provide a postcode."
}
],
"lVNV/d": [
{
"type": 0,
Expand Down Expand Up @@ -1619,12 +1635,6 @@
"value": "Use ⌘ + scroll to zoom the map"
}
],
"p+11YF": [
{
"type": 0,
"value": "You must provide a postcode."
}
],
"pguTkQ": [
{
"type": 0,
Expand Down
40 changes: 25 additions & 15 deletions src/i18n/compiled/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,16 @@
"value": "U moet akkoord gaan met het privacybeleid om door te gaan"
}
],
"8cxbC6": [
{
"type": 1,
"value": "field"
},
{
"type": 0,
"value": " is verplicht."
}
],
"8uBESg": [
{
"type": 0,
Expand Down Expand Up @@ -487,6 +497,12 @@
"value": "Contactgegevens"
}
],
"GbPef0": [
{
"type": 0,
"value": "Huisnummer is verplicht."
}
],
"GiRKAS": [
{
"type": 0,
Expand Down Expand Up @@ -814,7 +830,7 @@
"PCv4sQ": [
{
"type": 0,
"value": "Huis nummer"
"value": "Huisnummer"
}
],
"PjYrw0": [
Expand Down Expand Up @@ -1194,13 +1210,7 @@
"cBsrax": [
{
"type": 0,
"value": "Huis letter"
}
],
"cHn60V": [
{
"type": 0,
"value": "You must provide a house number."
"value": "Huisletter"
}
],
"cKFCTI": [
Expand Down Expand Up @@ -1491,10 +1501,16 @@
"value": "Product"
}
],
"lKBAu3": [
{
"type": 0,
"value": "Postcode is verplicht."
}
],
"lVNV/d": [
{
"type": 0,
"value": "Huis nummer toevoeging"
"value": "Huisnummertoevoeging"
}
],
"lY+Mza": [
Expand Down Expand Up @@ -1623,12 +1639,6 @@
"value": "Gebruik ⌘ + scroll om te zoomen in de kaart"
}
],
"p+11YF": [
{
"type": 0,
"value": "You must provide a postcode."
}
],
"pguTkQ": [
{
"type": 0,
Expand Down
Loading

0 comments on commit f152a1f

Please sign in to comment.