@defra/forms-engine-plugin 5.0.0-alpha.11 → 5.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.server/server/plugins/engine/components/CheckboxesField.d.ts +20 -2
  2. package/.server/server/plugins/engine/components/CheckboxesField.js +24 -12
  3. package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
  4. package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +2 -0
  5. package/.server/server/plugins/engine/components/EastingNorthingField.js +1 -1
  6. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
  7. package/.server/server/plugins/engine/components/FileUploadField.d.ts +10 -1
  8. package/.server/server/plugins/engine/components/FileUploadField.js +18 -6
  9. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  10. package/.server/server/plugins/engine/components/GeospatialField.d.ts +21 -0
  11. package/.server/server/plugins/engine/components/GeospatialField.js +32 -9
  12. package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -1
  13. package/.server/server/plugins/engine/components/LatLongField.d.ts +2 -0
  14. package/.server/server/plugins/engine/components/LocationFieldBase.js +4 -1
  15. package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -1
  16. package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +2 -0
  17. package/.server/server/plugins/engine/components/LocationFieldHelpers.js +7 -1
  18. package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -1
  19. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +5 -0
  20. package/.server/server/plugins/engine/components/helpers/geospatial.js +25 -16
  21. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
  22. package/.server/server/plugins/engine/form-availability.d.ts +3 -2
  23. package/.server/server/plugins/engine/form-availability.js +3 -2
  24. package/.server/server/plugins/engine/form-availability.js.map +1 -1
  25. package/.server/server/plugins/engine/form-context.js +2 -2
  26. package/.server/server/plugins/engine/form-context.js.map +1 -1
  27. package/.server/server/plugins/engine/i18n/buildValidationMessages.d.ts +1 -1
  28. package/.server/server/plugins/engine/i18n/buildValidationMessages.js +30 -9
  29. package/.server/server/plugins/engine/i18n/buildValidationMessages.js.map +1 -1
  30. package/.server/server/plugins/engine/i18n/translations/cy.json +66 -10
  31. package/.server/server/plugins/engine/i18n/translations/en-GB.json +29 -10
  32. package/.server/server/plugins/engine/i18n/types.d.ts +1 -0
  33. package/.server/server/plugins/engine/i18n/types.js.map +1 -1
  34. package/.server/server/plugins/engine/models/unavailable-view-model.d.ts +11 -2
  35. package/.server/server/plugins/engine/models/unavailable-view-model.js +23 -4
  36. package/.server/server/plugins/engine/models/unavailable-view-model.js.map +1 -1
  37. package/.server/server/plugins/engine/pageControllers/validationOptions.d.ts +1 -1
  38. package/.server/server/plugins/engine/pageControllers/validationOptions.js +1 -1
  39. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  40. package/.server/server/plugins/engine/unavailable-response.js +5 -2
  41. package/.server/server/plugins/engine/unavailable-response.js.map +1 -1
  42. package/.server/server/plugins/engine/views/components/_location-field-base.html +1 -1
  43. package/.server/server/plugins/engine/views/unavailable.html +14 -11
  44. package/package.json +1 -1
  45. package/src/server/plugins/engine/components/CheckboxesField.ts +34 -13
  46. package/src/server/plugins/engine/components/EastingNorthingField.ts +1 -1
  47. package/src/server/plugins/engine/components/FileUploadField.ts +26 -6
  48. package/src/server/plugins/engine/components/GeospatialField.ts +43 -11
  49. package/src/server/plugins/engine/components/LocationFieldBase.ts +3 -1
  50. package/src/server/plugins/engine/components/LocationFieldHelpers.ts +7 -1
  51. package/src/server/plugins/engine/components/helpers/geospatial.ts +32 -22
  52. package/src/server/plugins/engine/form-availability.ts +8 -2
  53. package/src/server/plugins/engine/form-context.ts +4 -2
  54. package/src/server/plugins/engine/i18n/buildValidationMessages.ts +15 -10
  55. package/src/server/plugins/engine/i18n/translations/cy.json +66 -10
  56. package/src/server/plugins/engine/i18n/translations/en-GB.json +29 -10
  57. package/src/server/plugins/engine/i18n/types.ts +1 -0
  58. package/src/server/plugins/engine/models/unavailable-view-model.ts +26 -5
  59. package/src/server/plugins/engine/pageControllers/validationOptions.ts +2 -2
  60. package/src/server/plugins/engine/unavailable-response.ts +6 -2
  61. package/src/server/plugins/engine/views/components/_location-field-base.html +1 -1
  62. package/src/server/plugins/engine/views/unavailable.html +14 -11
@@ -1,9 +1,10 @@
1
1
  import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model'
2
- import joi, { type ArraySchema, type LanguageMessages } from 'joi'
2
+ import joi, { type ArraySchema } from 'joi'
3
3
 
4
+ import { EN_GB } from '../../../constants.js'
4
5
  import { isFormValue } from './FormComponent.js'
5
6
  import { SelectionControlField } from './SelectionControlField.js'
6
- import { buildValidationMessages } from '../i18n/buildValidationMessages.js'
7
+ import { t as tPlugin } from '../i18n/index.js'
7
8
  import { type Translator } from '../i18n/types.js'
8
9
  import { type FormModel } from '../models/FormModel.js'
9
10
  import { type QuestionPageController } from '../pageControllers/QuestionPageController.js'
@@ -20,6 +21,7 @@ export class CheckboxesField extends SelectionControlField {
20
21
  declare schema: CheckboxesFieldComponent['schema']
21
22
  declare formSchema: ArraySchema<string> | ArraySchema<number>
22
23
  declare stateSchema: ArraySchema<string> | ArraySchema<number>
24
+ declare limits: { min?: number; max?: number; length?: number }
23
25
 
24
26
  constructor(
25
27
  def: CheckboxesFieldComponent,
@@ -42,11 +44,8 @@ export class CheckboxesField extends SelectionControlField {
42
44
  .single()
43
45
  .label(this.label)
44
46
  .required()
45
- .messages({
46
- 'array.min': messageTemplate.arrayMin,
47
- 'array.max': messageTemplate.arrayMax,
48
- 'array.length': messageTemplate.arrayLength
49
- })
47
+
48
+ const limits: { min?: number; max?: number; length?: number } = {}
50
49
 
51
50
  if (options.required === false) {
52
51
  formSchema = formSchema.optional()
@@ -54,16 +53,25 @@ export class CheckboxesField extends SelectionControlField {
54
53
 
55
54
  if (typeof schema?.length === 'number') {
56
55
  formSchema = formSchema.length(schema.length)
56
+ limits.length = schema.length
57
57
  } else {
58
58
  if (typeof schema?.min === 'number') {
59
59
  formSchema = formSchema.min(schema.min)
60
+ limits.min = schema.min
60
61
  }
61
62
 
62
63
  if (typeof schema?.max === 'number') {
63
64
  formSchema = formSchema.max(schema.max)
65
+ limits.max = schema.max
64
66
  }
65
67
  }
66
68
 
69
+ this.limits = limits
70
+
71
+ formSchema = formSchema.messages(
72
+ CheckboxesField.buildErrorMessages(EN_GB, limits)
73
+ )
74
+
67
75
  this.formSchema = formSchema.default([])
68
76
  this.stateSchema = formSchema.default(null).allow(null)
69
77
  this.options = options
@@ -135,13 +143,9 @@ export class CheckboxesField extends SelectionControlField {
135
143
  }
136
144
 
137
145
  getValidationMessagesOverride(translator: Translator) {
138
- const { t } = translator
146
+ const { language } = translator
139
147
  return {
140
- [this.name]: {
141
- 'array.min': buildValidationMessages(t).arrayMin,
142
- 'array.max': buildValidationMessages(t).arrayMax,
143
- 'array.length': buildValidationMessages(t).arrayLength
144
- } as LanguageMessages
148
+ [this.name]: CheckboxesField.buildErrorMessages(language, this.limits)
145
149
  }
146
150
  }
147
151
 
@@ -181,4 +185,21 @@ export class CheckboxesField extends SelectionControlField {
181
185
 
182
186
  return value.every(isFormValue)
183
187
  }
188
+
189
+ static buildErrorMessages(
190
+ language: string,
191
+ limits: { min?: number; max?: number; length?: number } = {}
192
+ ) {
193
+ return {
194
+ 'array.min': tPlugin('validation.arrayMin', language, {
195
+ count: limits.min ?? 1
196
+ }),
197
+ 'array.max': tPlugin('validation.arrayMax', language, {
198
+ count: limits.max ?? 1
199
+ }),
200
+ 'array.length': tPlugin('validation.arrayLength', language, {
201
+ count: limits.length ?? 1
202
+ })
203
+ }
204
+ }
184
205
  }
@@ -187,7 +187,7 @@ export class EastingNorthingField extends FormComponent {
187
187
  if (err.context?.country) {
188
188
  err.text = translator.t(
189
189
  'components.eastingNorthingField.wrongCountry',
190
- { country: err.context.country }
190
+ { country: translator.t(`common.${err.context.country}`) }
191
191
  )
192
192
  }
193
193
  })
@@ -113,6 +113,7 @@ export class FileUploadField extends FormComponent {
113
113
  declare schema: FileUploadFieldComponent['schema']
114
114
  declare formSchema: ArraySchema<FileState>
115
115
  declare stateSchema: ArraySchema<FileState>
116
+ declare limits: { min?: number; max?: number; length?: number }
116
117
 
117
118
  constructor(
118
119
  def: FileUploadFieldComponent,
@@ -132,21 +133,31 @@ export class FileUploadField extends FormComponent {
132
133
  formSchema = formSchema.optional()
133
134
  }
134
135
 
136
+ const limits: { min?: number; max?: number; length?: number } = {}
137
+
135
138
  if (typeof schema.length !== 'number') {
136
139
  if (typeof schema.max === 'number') {
137
140
  formSchema = formSchema.max(schema.max)
141
+ limits.max = schema.max
138
142
  }
139
143
 
140
144
  if (typeof schema.min === 'number') {
141
145
  formSchema = formSchema.min(schema.min)
146
+ limits.min = schema.min
142
147
  } else if (options.required !== false) {
143
148
  formSchema = formSchema.min(1)
149
+ limits.min = 1
144
150
  }
145
151
  } else {
146
152
  formSchema = formSchema.length(schema.length)
153
+ limits.length = schema.length
147
154
  }
148
155
 
149
- formSchema = formSchema.messages(FileUploadField.buildErrorMessages(EN_GB))
156
+ this.limits = limits
157
+
158
+ formSchema = formSchema.messages(
159
+ FileUploadField.buildErrorMessages(EN_GB, limits)
160
+ )
150
161
 
151
162
  this.formSchema = formSchema.items(formItemSchema)
152
163
  this.stateSchema = formSchema
@@ -309,7 +320,7 @@ export class FileUploadField extends FormComponent {
309
320
  getValidationMessagesOverride(translator: Translator) {
310
321
  const { language } = translator
311
322
  return {
312
- [this.name]: FileUploadField.buildErrorMessages(language)
323
+ [this.name]: FileUploadField.buildErrorMessages(language, this.limits)
313
324
  }
314
325
  }
315
326
 
@@ -417,11 +428,20 @@ export class FileUploadField extends FormComponent {
417
428
  }
418
429
  }
419
430
 
420
- static buildErrorMessages(language: string) {
431
+ static buildErrorMessages(
432
+ language: string,
433
+ limits: { min?: number; max?: number; length?: number } = {}
434
+ ) {
421
435
  return {
422
- 'array.min': tPlugin('validation.filesMin', language),
423
- 'array.max': tPlugin('validation.filesMax', language),
424
- 'array.length': tPlugin('validation.filesLength', language)
436
+ 'array.min': tPlugin('validation.filesMin', language, {
437
+ count: limits.min ?? 1
438
+ }),
439
+ 'array.max': tPlugin('validation.filesMax', language, {
440
+ count: limits.max ?? 1
441
+ }),
442
+ 'array.length': tPlugin('validation.filesLength', language, {
443
+ count: limits.length ?? 1
444
+ })
425
445
  }
426
446
  }
427
447
  }
@@ -1,14 +1,19 @@
1
1
  import { type GeospatialFieldComponent } from '@defra/forms-model'
2
2
  import { type ArraySchema } from 'joi'
3
3
 
4
+ import { EN_GB } from '../../../constants.js'
4
5
  import { type ComponentBase } from './ComponentBase.js'
5
6
  import {
6
7
  FormComponent,
7
8
  isGeospatialState
8
9
  } from './FormComponent.js'
9
10
  import { getMapLayers } from './LocationFieldHelpers.js'
10
- import { getGeospatialSchema } from './helpers/geospatial.js'
11
+ import {
12
+ determineLimits,
13
+ getGeospatialSchema
14
+ } from './helpers/geospatial.js'
11
15
  import { type RenderContext } from './types.js'
16
+ import { t as tPlugin } from '../i18n/index.js'
12
17
  import { type Translator } from '../i18n/types.js'
13
18
  import { messageTemplate } from '../pageControllers/validationOptions.js'
14
19
  import {
@@ -24,6 +29,7 @@ export class GeospatialField extends FormComponent {
24
29
  declare options: GeospatialFieldComponent['options']
25
30
  declare formSchema: ArraySchema<GeospatialState>
26
31
  declare stateSchema: ArraySchema<GeospatialState>
32
+ declare limits: { min?: number; max?: number; length?: number }
27
33
 
28
34
  constructor(
29
35
  def: GeospatialFieldComponent,
@@ -33,20 +39,24 @@ export class GeospatialField extends FormComponent {
33
39
 
34
40
  const { options } = def
35
41
 
36
- const formSchema = getGeospatialSchema(def).label(this.label).messages({
37
- 'array.min': messageTemplate.featuresMin,
38
- 'array.max': messageTemplate.featuresMax,
39
- 'array.length': messageTemplate.featuresLength
40
- })
42
+ let formSchema = getGeospatialSchema(def).label(this.label)
43
+
44
+ const isOptional = options.required === false
45
+ const limits = determineLimits(def, isOptional)
46
+
47
+ formSchema = formSchema.messages(
48
+ GeospatialField.buildErrorMessages(EN_GB, limits)
49
+ )
41
50
 
42
51
  this.formSchema = formSchema
43
52
  this.stateSchema = formSchema.default(null)
44
53
 
45
- if (options.required === false) {
54
+ if (isOptional) {
46
55
  this.stateSchema = this.stateSchema.allow(null)
47
56
  }
48
57
 
49
58
  this.options = options
59
+ this.limits = limits
50
60
  }
51
61
 
52
62
  getFormValueFromState(state: FormSubmissionState) {
@@ -127,9 +137,7 @@ export class GeospatialField extends FormComponent {
127
137
  'components.geospatialField.validation.wrongCountry',
128
138
  {
129
139
  count: Number(err.path[1]) + 1,
130
- country: translator.t(
131
- `common.${(err.context.country as string).toLowerCase()}`
132
- )
140
+ country: translator.t(`common.${err.context.country}`)
133
141
  }
134
142
  )
135
143
  }
@@ -149,6 +157,13 @@ export class GeospatialField extends FormComponent {
149
157
  return isGeospatialState(value)
150
158
  }
151
159
 
160
+ getValidationMessagesOverride(translator: Translator) {
161
+ const { language } = translator
162
+ return {
163
+ [this.name]: GeospatialField.buildErrorMessages(language, this.limits)
164
+ }
165
+ }
166
+
152
167
  /**
153
168
  * For error preview page that shows all possible errors on a component
154
169
  */
@@ -169,11 +184,28 @@ export class GeospatialField extends FormComponent {
169
184
  { type: 'required', template: messageTemplate.selectRequired },
170
185
  {
171
186
  type: 'array.min',
172
- template: '{{#title}} must contain at least 1 items'
187
+ template: '{{#title}} must contain at least 1 item'
173
188
  },
174
189
  { type: 'object.invalidjson', template: messageTemplate.format }
175
190
  ],
176
191
  advancedSettingsErrors: []
177
192
  }
178
193
  }
194
+
195
+ static buildErrorMessages(
196
+ language: string,
197
+ limits: { min?: number; max?: number; length?: number } = {}
198
+ ) {
199
+ return {
200
+ 'array.min': tPlugin('validation.featuresMin', language, {
201
+ count: limits.min ?? 1
202
+ }),
203
+ 'array.max': tPlugin('validation.featuresMax', language, {
204
+ count: limits.max ?? 1
205
+ }),
206
+ 'array.length': tPlugin('validation.featuresLength', language, {
207
+ count: limits.length ?? 1
208
+ })
209
+ }
210
+ }
179
211
  }
@@ -125,9 +125,11 @@ export abstract class LocationFieldBase extends FormComponent {
125
125
  const viewModel = super.getViewModel(context)
126
126
 
127
127
  if (this.instructionText) {
128
+ const { tComponent } = context.translator
128
129
  return {
129
130
  ...viewModel,
130
- instructionText: this.instructionText
131
+ instructionText:
132
+ tComponent(this.def, 'instructionText') || this.instructionText
131
133
  }
132
134
  }
133
135
 
@@ -198,9 +198,15 @@ export function getLocationFieldViewModel(
198
198
  }
199
199
 
200
200
  if (component.options.instructionText) {
201
+ const { t, tComponent } = context.translator
202
+
201
203
  return {
202
204
  ...result,
203
- instructionText: component.options.instructionText
205
+ instructionSummary: t('components.locationFieldBase.howToFind'),
206
+ instructionText:
207
+ tComponent(component.def, 'instructionText') ||
208
+ component.options.instructionText,
209
+ t
204
210
  }
205
211
  }
206
212
 
@@ -1,5 +1,4 @@
1
1
  import {
2
- GeospatialFieldOptionsCountryEnum,
3
2
  type GeospatialFieldComponent,
4
3
  type GeospatialFieldOptionsCountry
5
4
  } from '@defra/forms-model'
@@ -20,13 +19,6 @@ import {
20
19
  } from '../../types.js'
21
20
  import { countries } from '../../../map/routes/index.js'
22
21
 
23
- const countriesDesc: Record<GeospatialFieldOptionsCountryEnum, string> = {
24
- [GeospatialFieldOptionsCountryEnum.England]: 'England',
25
- [GeospatialFieldOptionsCountryEnum.NorthernIreland]: 'Northern Ireland',
26
- [GeospatialFieldOptionsCountryEnum.Scotland]: 'Scotland',
27
- [GeospatialFieldOptionsCountryEnum.Wales]: 'Wales'
28
- }
29
-
30
22
  const Joi = JoiBase.extend({
31
23
  type: 'array',
32
24
  base: JoiBase.array(),
@@ -103,25 +95,43 @@ const featureSchema = Joi.object<Feature>().keys({
103
95
  geometry: featureGeometrySchema
104
96
  })
105
97
 
106
- function applySchemaConstraints(
107
- schema: JoiBase.ArraySchema<Feature[]>,
108
- def: GeospatialFieldComponent
98
+ export function determineLimits(
99
+ def: GeospatialFieldComponent,
100
+ isOptional: boolean
109
101
  ) {
110
- const { options, schema: constraints } = def
111
- const isOptional = options.required === false
112
-
102
+ const { schema: constraints } = def
103
+ const limits: { min?: number; max?: number; length?: number } = {}
113
104
  if (typeof constraints?.length === 'number') {
114
- schema = schema.length(constraints.length)
105
+ limits.length = constraints.length
115
106
  } else {
116
107
  if (typeof constraints?.min === 'number') {
117
- schema = schema.min(constraints.min)
108
+ limits.min = constraints.min
118
109
  } else if (!isOptional) {
119
- schema = schema.min(1)
110
+ limits.min = 1
120
111
  }
121
112
 
122
- schema = schema.max(
123
- typeof constraints?.max === 'number' ? constraints.max : 50
124
- )
113
+ limits.max = typeof constraints?.max === 'number' ? constraints.max : 50
114
+ }
115
+ return limits
116
+ }
117
+
118
+ function applySchemaConstraints(
119
+ schema: JoiBase.ArraySchema<Feature[]>,
120
+ def: GeospatialFieldComponent
121
+ ) {
122
+ const { options } = def
123
+ const isOptional = options.required === false
124
+
125
+ const limits = determineLimits(def, isOptional)
126
+
127
+ if (limits.length !== undefined) {
128
+ schema = schema.length(limits.length)
129
+ }
130
+ if (limits.min !== undefined) {
131
+ schema = schema.min(limits.min)
132
+ }
133
+ if (limits.max !== undefined) {
134
+ schema = schema.max(limits.max)
125
135
  }
126
136
 
127
137
  if (isOptional) {
@@ -161,7 +171,7 @@ export function getGeospatialSchema(
161
171
 
162
172
  if (!result) {
163
173
  return helpers.error('any.custom', {
164
- country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]
174
+ country
165
175
  })
166
176
  }
167
177
 
@@ -212,7 +222,7 @@ export function getEastingNorthingCountryValidator(
212
222
  return helpers.error(
213
223
  'any.custom',
214
224
  {
215
- country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]
225
+ country
216
226
  },
217
227
  { path: [field.name] }
218
228
  )
@@ -1,9 +1,14 @@
1
- import { FormStatus, type FormMetadata } from '@defra/forms-model'
1
+ import {
2
+ FormStatus,
3
+ type FormDefinition,
4
+ type FormMetadata
5
+ } from '@defra/forms-model'
2
6
  import Boom from '@hapi/boom'
3
7
 
4
8
  export interface OfflineBoomData {
5
9
  offline: true
6
10
  metadata: FormMetadata
11
+ definition: FormDefinition | undefined
7
12
  }
8
13
 
9
14
  /**
@@ -13,6 +18,7 @@ export interface OfflineBoomData {
13
18
  */
14
19
  export function assertFormAvailable(
15
20
  metadata: FormMetadata,
21
+ definition: FormDefinition | undefined,
16
22
  formState: FormStatus,
17
23
  isPreview: boolean
18
24
  ): void {
@@ -21,7 +27,7 @@ export function assertFormAvailable(
21
27
  formState === FormStatus.Live &&
22
28
  !isPreview
23
29
  ) {
24
- const data: OfflineBoomData = { offline: true, metadata }
30
+ const data: OfflineBoomData = { offline: true, metadata, definition }
25
31
  throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {
26
32
  statusCode: 503,
27
33
  data
@@ -56,13 +56,14 @@ export async function getFormModel(
56
56
  const formState = resolveState(state)
57
57
 
58
58
  const metadata = await formsService.getFormMetadata(slug)
59
- assertFormAvailable(metadata, formState, isPreview)
60
59
 
61
60
  const definition = await formsService.getFormDefinition(
62
61
  metadata.id,
63
62
  formState
64
63
  )
65
64
 
65
+ assertFormAvailable(metadata, definition, formState, isPreview)
66
+
66
67
  if (!definition) {
67
68
  throw Boom.notFound(
68
69
  `No definition found for form metadata ${metadata.id} (${slug}) ${state}`
@@ -145,7 +146,6 @@ export async function resolveFormModel(
145
146
  const metadata = await formsService.getFormMetadata(slug)
146
147
  const formState = resolveState(state)
147
148
  const isPreview = options.isPreview ?? isPreviewState(state, options)
148
- assertFormAvailable(metadata, formState, isPreview)
149
149
  const stateMetadata = metadata[formState]
150
150
 
151
151
  if (!stateMetadata) {
@@ -199,6 +199,8 @@ export async function resolveFormModel(
199
199
  cache.set(cacheKey, entry)
200
200
  }
201
201
 
202
+ assertFormAvailable(metadata, entry.model.def, formState, isPreview)
203
+
202
204
  return entry.model
203
205
  }
204
206
 
@@ -16,7 +16,9 @@ const opts = {
16
16
  * because Joi cannot resolve custom functions from a plain string.
17
17
  * Plain strings with only built-in tokens ({{#label}}, {{#limit}}) work without wrapping.
18
18
  */
19
- export function buildValidationMessages(t: (key: string) => string) {
19
+ export function buildValidationMessages(
20
+ t: (key: string, options?: Record<string, unknown>) => string
21
+ ) {
20
22
  return {
21
23
  // Expressions — contain lowerFirst custom function
22
24
  required: joi.expression(t('validation.required'), opts) as JoiExpression,
@@ -57,15 +59,18 @@ export function buildValidationMessages(t: (key: string) => string) {
57
59
  dateMax: t('components.dateField.validation.dateMax'),
58
60
 
59
61
  unicode: t('validation.unicode'),
60
- arrayMin: t('validation.arrayMin'),
61
- arrayMax: t('validation.arrayMax'),
62
- arrayLength: t('validation.arrayLength'),
63
- featuresMin: t('validation.featuresMin'),
64
- featuresMax: t('validation.featuresMax'),
65
- featuresLength: t('validation.featuresLength'),
66
- filesMin: t('validation.filesMin'),
67
- filesMax: t('validation.filesMax'),
68
- filesLength: t('validation.filesLength')
62
+ // These generic templates are shared across fields with differing limits, so a
63
+ // fixed singular count is used here — components needing accurate singular/plural
64
+ // text (e.g. FileUploadField) build their own per-instance messages instead.
65
+ arrayMin: t('validation.arrayMin', { count: 1 }),
66
+ arrayMax: t('validation.arrayMax', { count: 1 }),
67
+ arrayLength: t('validation.arrayLength', { count: 1 }),
68
+ featuresMin: t('validation.featuresMin', { count: 1 }),
69
+ featuresMax: t('validation.featuresMax', { count: 1 }),
70
+ featuresLength: t('validation.featuresLength', { count: 1 }),
71
+ filesMin: t('validation.filesMin', { count: 1 }),
72
+ filesMax: t('validation.filesMax', { count: 1 }),
73
+ filesLength: t('validation.filesLength', { count: 1 })
69
74
  }
70
75
  }
71
76
 
@@ -16,15 +16,61 @@
16
16
  "numberMax": "Rhaid i {{#label}} fod yn {{#limit}} neu is",
17
17
  "maxWords": "Rhaid i {{#label}} fod yn {{#limit}} o eiriau neu lai",
18
18
  "unicode": "Mae {{#label}} yn cynnwys nodau annilys, er enghraifft, dashes hirion",
19
- "arrayMin": "Dewiswch o leiaf {{#limit}} o opsiynau o'r rhestr",
20
- "arrayMax": "Dim ond {{#limit}} y gellir eu dewis o'r rhestr",
21
- "arrayLength": "Dewiswch ddim ond {{#limit}} o opsiynau o'r rhestr",
22
- "featuresMin": "Diffiniwch o leiaf {{#limit}} nodwedd",
23
- "featuresMax": "Dim ond {{#limit}} nodwedd y gellir eu diffinio",
24
- "featuresLength": "Diffiniwch nodweddion {{#limit}} yn union",
25
- "filesMin": "Rhaid i chi uwchlwytho {{#limit}} o ffeiliau neu fwy",
26
- "filesMax": "Dim ond {{#limit}} o ffeiliau neu lai y gallwch eu huwchlwytho",
27
- "filesLength": "Rhaid i chi uwchlwytho'n union {{#limit}} o ffeiliau"
19
+ "arrayMin_zero": "Dewiswch o leiaf 1 opsiwn o'r rhestr",
20
+ "arrayMin_one": "Dewiswch o leiaf {{#limit}} opsiwn o'r rhestr",
21
+ "arrayMin_two": "Dewiswch o leiaf ddau opsiwn o'r rhestr",
22
+ "arrayMin_few": "Dewiswch o leiaf {{#limit}} opsiwn o'r rhestr",
23
+ "arrayMin_many": "Dewiswch o leiaf {{#limit}} opsiwn o'r rhestr",
24
+ "arrayMin_other": "Dewiswch o leiaf {{#limit}} opsiwn o'r rhestr",
25
+ "arrayMax_zero": "Dim ond {{#limit}} opsiynau y gellir eu dewis o'r rhestr",
26
+ "arrayMax_one": "Dim ond {{#limit}} opsiwn y gellir ei ddewis o'r rhestr",
27
+ "arrayMax_two": "Dim ond dwy opsiwn y gellir eu dewis o'r rhestr",
28
+ "arrayMax_few": "Dim ond {{#limit}} opsiwn y gellir eu dewis o'r rhestr",
29
+ "arrayMax_many": "Dim ond {{#limit}} opsiwn y gellir eu dewis o'r rhestr",
30
+ "arrayMax_other": "Dim ond {{#limit}} opsiwn y gellir eu dewis o'r rhestr",
31
+ "arrayLength_zero": "Dewiswch 0 opsiwn yn unig o'r rhestr",
32
+ "arrayLength_one": "Dewiswch un opsiwn yn unig o'r rhestr",
33
+ "arrayLength_two": "Dewiswch {{#limit}} opsiwn yn unig o'r rhestr",
34
+ "arrayLength_few": "Dewiswch {{#limit}} opsiwn yn unig o'r rhestr",
35
+ "arrayLength_many": "Dewiswch {{#limit}} opsiwn yn unig o'r rhestr",
36
+ "arrayLength_other": "Dewiswch {{#limit}} opsiwn yn unig o'r rhestr",
37
+
38
+ "featuresMin_zero": "Diffiniwch o leiaf {{#limit}} nodwedd",
39
+ "featuresMin_one": "Diffiniwch o leiaf un nodwedd",
40
+ "featuresMin_two": "Diffiniwch o leiaf {{#limit}} nodwedd",
41
+ "featuresMin_few": "Diffiniwch o leiaf {{#limit}} nodwedd",
42
+ "featuresMin_many": "Diffiniwch o leiaf {{#limit}} nodwedd",
43
+ "featuresMin_other": "Diffiniwch o leiaf {{#limit}} nodwedd",
44
+ "featuresMax_zero": "Dim ond {{#limit}} nodwedd y gellir eu diffinio",
45
+ "featuresMax_one": "Dim ond {{#limit}} nodwedd y gellir ei diffinio",
46
+ "featuresMax_two": "Dim ond {{#limit}} nodwedd y gellir eu diffinio",
47
+ "featuresMax_few": "Dim ond {{#limit}} nodwedd y gellir eu diffinio",
48
+ "featuresMax_many": "Dim ond {{#limit}} nodwedd y gellir eu diffinio",
49
+ "featuresMax_other": "Dim ond {{#limit}} nodwedd y gellir eu diffinio",
50
+ "featuresLength_zero": "Diffiniwch yn union {{#limit}} nodwedd",
51
+ "featuresLength_one": "Diffiniwch un nodwedd yn union",
52
+ "featuresLength_two": "Diffiniwch nodweddion {{#limit}} yn union",
53
+ "featuresLength_few": "Diffiniwch nodweddion {{#limit}} yn union",
54
+ "featuresLength_many": "Diffiniwch nodweddion {{#limit}} yn union",
55
+ "featuresLength_other": "Diffiniwch nodweddion {{#limit}} yn union",
56
+ "filesMin_zero": "Rhaid i chi uwchlwytho {{#limit}} ffeil neu fwy",
57
+ "filesMin_one": "Rhaid i chi uwchlwytho un ffeil neu fwy",
58
+ "filesMin_two": "Rhaid i chi uwchlwytho {{#limit}} o ffeiliau neu fwy",
59
+ "filesMin_few": "Rhaid i chi uwchlwytho {{#limit}} o ffeiliau neu fwy",
60
+ "filesMin_many": "Rhaid i chi uwchlwytho {{#limit}} o ffeiliau neu fwy",
61
+ "filesMin_other": "Rhaid i chi uwchlwytho {{#limit}} o ffeiliau neu fwy",
62
+ "filesMax_zero": "Dim ond {{#limit}} ffeil neu lai y gallwch eu huwchlwytho",
63
+ "filesMax_one": "Dim ond un ffeil neu lai y gallwch ei huwchlwytho",
64
+ "filesMax_two": "Dim ond {{#limit}} o ffeiliau neu lai y gallwch eu huwchlwytho",
65
+ "filesMax_few": "Dim ond {{#limit}} o ffeiliau neu lai y gallwch eu huwchlwytho",
66
+ "filesMax_many": "Dim ond {{#limit}} o ffeiliau neu lai y gallwch eu huwchlwytho",
67
+ "filesMax_other": "Dim ond {{#limit}} o ffeiliau neu lai y gallwch eu huwchlwytho",
68
+ "filesLength_zero": "Rhaid i chi uwchlwytho union {{#limit}} ffeil",
69
+ "filesLength_one": "Rhaid i chi uwchlwytho union un ffeil",
70
+ "filesLength_two": "Rhaid i chi uwchlwytho'n union {{#limit}} o ffeiliau",
71
+ "filesLength_few": "Rhaid i chi uwchlwytho'n union {{#limit}} o ffeiliau",
72
+ "filesLength_many": "Rhaid i chi uwchlwytho'n union {{#limit}} o ffeiliau",
73
+ "filesLength_other": "Rhaid i chi uwchlwytho'n union {{#limit}} o ffeiliau"
28
74
  },
29
75
 
30
76
  "errors": {
@@ -44,7 +90,7 @@
44
90
  "and": "a",
45
91
  "england": "Lloegr",
46
92
  "wales": "Cymru",
47
- "northernIreland": "Gogledd Iwerddon",
93
+ "northern-ireland": "Gogledd Iwerddon",
48
94
  "scotland": "Yr Alban",
49
95
  "sendFeedback": "Anfon adborth"
50
96
  },
@@ -161,6 +207,16 @@
161
207
  "bannerForce": "Rhagolwg o ffurflen [[previewMode]] yw hon. Peidiwch â rhoi gwybodaeth bersonol.",
162
208
  "bannerText": "Dyma ragolwg o dudalen ffurflen [[previewMode]] rydych chi'n ei golygu.",
163
209
  "previousPages": "Mae'n dibynnu ar atebion o dudalennau cynharach yn y ffurflen. Yn y fersiwn fyw, bydd angen i ddefnyddwyr gwblhau'r cwestiynau hynny yn gyntaf."
210
+ },
211
+
212
+ "formUnavailable": {
213
+ "title": "Mae'n ddrwg gennyf, nid yw'r ffurflen hon ar gael",
214
+ "subHeading": "Nid yw '[[formTitle]]' ar gael mwyach.",
215
+ "contactDetailsHeading": "Manylion cyswllt ar gyfer [[organisationName]]",
216
+ "telephone": "Ffôn",
217
+ "callCharges": "Dysgwch am gostau galwadau",
218
+ "email": "E-bost",
219
+ "onlineContactForm": "Ffurflen gyswllt ar-lein"
164
220
  }
165
221
  },
166
222