@defra/forms-model 3.0.687 → 3.0.689

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 (54) hide show
  1. package/dist/module/__stubs__/examples.js +185 -0
  2. package/dist/module/__stubs__/examples.js.map +1 -0
  3. package/dist/module/components/helpers.js +4 -0
  4. package/dist/module/components/helpers.js.map +1 -1
  5. package/dist/module/components/types.js.map +1 -1
  6. package/dist/module/form/form-editor/translations/translations-config.js +168 -0
  7. package/dist/module/form/form-editor/translations/translations-config.js.map +1 -0
  8. package/dist/module/form/form-editor/translations/translations-overview.js +93 -0
  9. package/dist/module/form/form-editor/translations/translations-overview.js.map +1 -0
  10. package/dist/module/form/form-editor/translations/translations.js +214 -0
  11. package/dist/module/form/form-editor/translations/translations.js.map +1 -0
  12. package/dist/module/form/form-submission/index.js +3 -0
  13. package/dist/module/form/form-submission/index.js.map +1 -1
  14. package/dist/module/form/form-submission/types.js.map +1 -1
  15. package/dist/module/form/utils/list.js +23 -0
  16. package/dist/module/form/utils/list.js.map +1 -1
  17. package/dist/module/form/utils/numbering.js +43 -0
  18. package/dist/module/form/utils/numbering.js.map +1 -0
  19. package/dist/module/index.js +2 -0
  20. package/dist/module/index.js.map +1 -1
  21. package/dist/types/__stubs__/examples.d.ts +9 -0
  22. package/dist/types/__stubs__/examples.d.ts.map +1 -0
  23. package/dist/types/components/helpers.d.ts +2 -1
  24. package/dist/types/components/helpers.d.ts.map +1 -1
  25. package/dist/types/components/types.d.ts +1 -0
  26. package/dist/types/components/types.d.ts.map +1 -1
  27. package/dist/types/form/form-editor/translations/translations-config.d.ts +95 -0
  28. package/dist/types/form/form-editor/translations/translations-config.d.ts.map +1 -0
  29. package/dist/types/form/form-editor/translations/translations-overview.d.ts +14 -0
  30. package/dist/types/form/form-editor/translations/translations-overview.d.ts.map +1 -0
  31. package/dist/types/form/form-editor/translations/translations.d.ts +16 -0
  32. package/dist/types/form/form-editor/translations/translations.d.ts.map +1 -0
  33. package/dist/types/form/form-submission/index.d.ts.map +1 -1
  34. package/dist/types/form/form-submission/types.d.ts +4 -0
  35. package/dist/types/form/form-submission/types.d.ts.map +1 -1
  36. package/dist/types/form/utils/list.d.ts +8 -1
  37. package/dist/types/form/utils/list.d.ts.map +1 -1
  38. package/dist/types/form/utils/numbering.d.ts +18 -0
  39. package/dist/types/form/utils/numbering.d.ts.map +1 -0
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/schemas/form-submit-payload-schema.json +5 -0
  44. package/src/__stubs__/examples.js +231 -0
  45. package/src/components/helpers.ts +14 -0
  46. package/src/components/types.ts +7 -0
  47. package/src/form/form-editor/translations/translations-config.js +186 -0
  48. package/src/form/form-editor/translations/translations-overview.js +137 -0
  49. package/src/form/form-editor/translations/translations.js +297 -0
  50. package/src/form/form-submission/index.ts +7 -0
  51. package/src/form/form-submission/types.ts +5 -0
  52. package/src/form/utils/list.ts +34 -1
  53. package/src/form/utils/numbering.js +47 -0
  54. package/src/index.ts +2 -0
@@ -0,0 +1,186 @@
1
+ import { ComponentType } from '~/src/components/enums.js'
2
+
3
+ /**
4
+ * @typedef {object} TranslationRow
5
+ * @property {string} name - the key name
6
+ * @property {string} [type] - type of text
7
+ * @property {string | undefined} englishContent - English text
8
+ * @property {string | undefined} welshContent - Welsh text
9
+ * @property {number} [pageNum] - page number
10
+ * @property {number} [questionNum] - question number
11
+ * @property {number} [itemNum] - list item number
12
+ * @property {string} [label] - label for field
13
+ */
14
+
15
+ /**
16
+ * @typedef {{ text: string, href?: string }} ErrorDetailsItem
17
+ * @typedef {Record<string, ErrorDetailsItem>} ErrorDetails
18
+ */
19
+
20
+ /**
21
+ * @template {object} [Schema=object]
22
+ * @typedef {object} ValidationFailure
23
+ * @property {ErrorDetails} formErrors - Formatted errors for error summary
24
+ * @property {Schema} formValues - Form POST payload from Hapi request
25
+ */
26
+
27
+ export const TranslationRowTypes = {
28
+ PageHeading: 'PageHeading',
29
+ PageGuidance: 'PageGuidance',
30
+ RepeatTitle: 'RepeatTitle',
31
+ QuestionText: 'QuestionText',
32
+ QuestionHint: 'QuestionHint',
33
+ ShortDescription: 'ShortDescription',
34
+ ErrorDescription: 'ErrorDescription',
35
+ ListItemText: 'ListItemText',
36
+ ListItemHint: 'ListItemHint',
37
+ InstructionText: 'InstructionText',
38
+ DeclarationBody: 'DeclarationBody',
39
+ PaymentDescription: 'PaymentDescription'
40
+ }
41
+
42
+ export const TEXTAREA_3_ROWS = 'textarea3'
43
+ export const TEXTAREA_5_ROWS = 'textarea5'
44
+ export const TEXTAREA_12_ROWS_WITH_MARKDOWN = 'textarea12markdown'
45
+ export const LIST_ITEM_HINT = 'listitemhint'
46
+ export const LIST_ITEM_WITH_HINT_FOLLOWING = 'listoptionwithhintfollowing'
47
+
48
+ /** @type {Record<string, { jsonPrefix: string, jsonSuffix: string, displayName: string, getLabel: (pageNum: number, questionNum?: number, itemNum?: number) => string }>} */
49
+ export const keyConfig = {
50
+ [TranslationRowTypes.QuestionText]: {
51
+ jsonPrefix: 'components',
52
+ jsonSuffix: 'title',
53
+ displayName: 'Question text',
54
+ getLabel: (pageNum, questionNum) =>
55
+ `Welsh question text - Page ${pageNum}, question ${questionNum}`
56
+ },
57
+ [TranslationRowTypes.QuestionHint]: {
58
+ jsonPrefix: 'components',
59
+ jsonSuffix: 'hint',
60
+ displayName: 'Hint',
61
+ getLabel: (pageNum, questionNum) =>
62
+ `Welsh hint - Page ${pageNum}, question ${questionNum}`
63
+ },
64
+ [TranslationRowTypes.ShortDescription]: {
65
+ jsonPrefix: 'components',
66
+ jsonSuffix: 'shortDescription',
67
+ displayName: 'Short description',
68
+ getLabel: (pageNum, questionNum) =>
69
+ `Welsh short description - Page ${pageNum}, question ${questionNum}`
70
+ },
71
+ [TranslationRowTypes.InstructionText]: {
72
+ jsonPrefix: 'components',
73
+ jsonSuffix: 'instructionText',
74
+ displayName: 'Instruction text',
75
+ getLabel: (pageNum, questionNum) =>
76
+ `Welsh instruction text - Page ${pageNum}, question ${questionNum}`
77
+ },
78
+ [TranslationRowTypes.DeclarationBody]: {
79
+ jsonPrefix: 'components',
80
+ jsonSuffix: 'content',
81
+ displayName: 'Declaration body',
82
+ getLabel: (pageNum, questionNum) =>
83
+ `Welsh declaration body - Page ${pageNum}, question ${questionNum}`
84
+ },
85
+ [TranslationRowTypes.PaymentDescription]: {
86
+ jsonPrefix: 'components',
87
+ jsonSuffix: 'paymentDescription',
88
+ displayName: 'Payment description',
89
+ getLabel: (pageNum, questionNum) =>
90
+ `Welsh payment description - Page ${pageNum}, question ${questionNum}`
91
+ },
92
+ [TranslationRowTypes.ListItemText]: {
93
+ jsonPrefix: 'listItems',
94
+ jsonSuffix: 'text',
95
+ displayName: 'Option',
96
+ getLabel: (pageNum, questionNum, itemNum) =>
97
+ `Welsh option ${itemNum} - Page ${pageNum}, question ${questionNum}`
98
+ },
99
+ [TranslationRowTypes.ListItemHint]: {
100
+ jsonPrefix: 'listItems',
101
+ jsonSuffix: 'hint',
102
+ displayName: 'Option hint',
103
+ getLabel: (pageNum, questionNum, itemNum) =>
104
+ `Welsh hint for option ${itemNum} - Page ${pageNum}, question ${questionNum}`
105
+ },
106
+ [TranslationRowTypes.PageGuidance]: {
107
+ jsonPrefix: 'components',
108
+ jsonSuffix: 'content',
109
+ displayName: 'Guidance text',
110
+ getLabel: (pageNum) => `Welsh guidance text (markdown) - page ${pageNum}`
111
+ },
112
+ [TranslationRowTypes.PageHeading]: {
113
+ jsonPrefix: 'pages',
114
+ jsonSuffix: 'title',
115
+ displayName: 'Page heading',
116
+ getLabel: (pageNum) => `Welsh page heading - page ${pageNum}`
117
+ },
118
+ [TranslationRowTypes.RepeatTitle]: {
119
+ jsonPrefix: 'pages',
120
+ jsonSuffix: 'repeatTitle',
121
+ displayName: 'Repeat name',
122
+ getLabel: (pageNum) => `Welsh repeat name - page ${pageNum}`
123
+ }
124
+ }
125
+
126
+ export const IGNORE_FIELDS = [
127
+ ComponentType.Details,
128
+ ComponentType.Html,
129
+ ComponentType.InsetText,
130
+ ComponentType.HiddenField
131
+ ]
132
+
133
+ /**
134
+ * Lookup a translation from a list of translation records.
135
+ * @param {string} key - The translation key to look up.
136
+ * @param {Record<string, string> | undefined} translations - The translation values to search.
137
+ */
138
+ export function lookupTranslation(key, translations) {
139
+ if (!translations) {
140
+ return ''
141
+ }
142
+ return translations[key] ?? ''
143
+ }
144
+
145
+ /**
146
+ * Resolve the underlying content for a translation row from an entity.
147
+ * Involves using the appropriate child properties if necessary.
148
+ * @param {string} keyType - The translation row type being resolved.
149
+ * @param {ComponentDef | Page | Item} entity - The entity that may contain the content to translate.
150
+ * @param {string} jsonSuffix - The property name to read from the entity.
151
+ */
152
+ export function drillDown(keyType, entity, jsonSuffix) {
153
+ /** @type {Partial<Record<string, (entity: ComponentDef | Page | Item) => string>>} */
154
+ const resolveData = {
155
+ [TranslationRowTypes.InstructionText]: () =>
156
+ // @ts-expect-error - dynamic property lookup
157
+ 'options' in entity ? entity.options?.instructionText : '',
158
+ [TranslationRowTypes.ListItemHint]: () =>
159
+ // @ts-expect-error - dynamic property lookup
160
+ 'hint' in entity ? entity.hint.text : '',
161
+ [TranslationRowTypes.DeclarationBody]: () =>
162
+ 'content' in entity ? entity.content : '',
163
+ [TranslationRowTypes.PaymentDescription]: () =>
164
+ // @ts-expect-error - dynamic property lookup
165
+ 'options' in entity ? entity.options?.description : ''
166
+ }
167
+ const func = resolveData[keyType]
168
+ if (func) {
169
+ return func(entity)
170
+ } else {
171
+ const entityWithDynamicProperties = /** @type {Record<string, unknown>} */ (
172
+ /** @type {unknown} */ (entity)
173
+ )
174
+ if (typeof entity === 'object') {
175
+ return jsonSuffix in entityWithDynamicProperties
176
+ ? /** @type {string} */ (entityWithDynamicProperties[jsonSuffix])
177
+ : ''
178
+ }
179
+ }
180
+ return entity
181
+ }
182
+
183
+ /**
184
+ * @import { ComponentDef } from '~/src/components/types.js'
185
+ * @import { Item, Page } from '~/src/form/form-definition/types.js'
186
+ */
@@ -0,0 +1,137 @@
1
+ import { lookupTranslation } from '~/src/form/form-editor/translations/translations-config.js'
2
+
3
+ const formNameKey = 'form.title'
4
+ const contactEmailAddressKey = 'form.contact.email.address'
5
+ const contactEmailResponseKey = 'form.contact.email.responseTime'
6
+ const contactOnlineUrlKey = 'form.contact.online.url'
7
+ const contactOnlineTextKey = 'form.contact.online.text'
8
+ const contactPhoneKey = 'form.contact.phone'
9
+ const submissionGuidanceKey = 'form.submissionGuidance'
10
+ const privacyNoticeUrlKey = 'form.privacyNoticeUrl'
11
+ const privacyNoticeTextKey = 'form.privacyNoticeText'
12
+
13
+ /**
14
+ * Determine a field value, either from the user-entered payload (validation) or from a translation lookup
15
+ * @param {string} key
16
+ * @param {Record<string, string>} translations
17
+ * @param {ValidationFailure<any>} [validation]
18
+ * @returns { string | undefined }
19
+ */
20
+ function getTranslation(key, translations, validation) {
21
+ return validation?.formValues[key] ?? lookupTranslation(key, translations)
22
+ }
23
+
24
+ /**
25
+ * Build all translation rows in the form overview section (metatdata values).
26
+ * @param {FormMetadata} metadata
27
+ * @param {FormDefinition} definition
28
+ * @param {Record<string, string>} translations
29
+ * @param {ValidationFailure<any>} [validation]
30
+ * @returns {TranslationRow[]}
31
+ */
32
+ export function buildOverviewSection(
33
+ metadata,
34
+ definition,
35
+ translations,
36
+ validation
37
+ ) {
38
+ /** @type {TranslationRow[]} */
39
+ const rows = []
40
+
41
+ rows.push(
42
+ {
43
+ name: formNameKey,
44
+ englishContent: definition.name,
45
+ welshContent: getTranslation(formNameKey, translations, validation),
46
+ label: 'Welsh form name'
47
+ },
48
+ {
49
+ name: contactEmailAddressKey,
50
+ englishContent: metadata.contact?.email?.address ?? 'Not set',
51
+ welshContent: getTranslation(
52
+ contactEmailAddressKey,
53
+ translations,
54
+ validation
55
+ ),
56
+ label: 'Welsh support email address'
57
+ },
58
+ {
59
+ name: contactEmailResponseKey,
60
+ englishContent: metadata.contact?.email?.responseTime ?? '--',
61
+ welshContent: getTranslation(
62
+ contactEmailResponseKey,
63
+ translations,
64
+ validation
65
+ ),
66
+ label: 'Welsh response time message'
67
+ },
68
+ {
69
+ name: contactOnlineUrlKey,
70
+ englishContent: metadata.contact?.online?.url ?? 'Not set',
71
+ welshContent: getTranslation(
72
+ contactOnlineUrlKey,
73
+ translations,
74
+ validation
75
+ ),
76
+ label: 'Welsh contact link URL'
77
+ },
78
+ {
79
+ name: contactOnlineTextKey,
80
+ englishContent: metadata.contact?.online?.text ?? '--',
81
+ welshContent: getTranslation(
82
+ contactOnlineTextKey,
83
+ translations,
84
+ validation
85
+ ),
86
+ label: 'Welsh text for the contact link'
87
+ },
88
+ {
89
+ name: contactPhoneKey,
90
+ englishContent: metadata.contact?.phone ?? 'Not set',
91
+ welshContent: getTranslation(contactPhoneKey, translations, validation),
92
+ label: 'Welsh phone number and opening times'
93
+ },
94
+ {
95
+ name: submissionGuidanceKey,
96
+ englishContent: metadata.submissionGuidance ?? 'Not set',
97
+ welshContent: getTranslation(
98
+ submissionGuidanceKey,
99
+ translations,
100
+ validation
101
+ ),
102
+ label: 'Welsh information about what happens next'
103
+ }
104
+ )
105
+
106
+ if (metadata.privacyNoticeType === 'link') {
107
+ rows.push({
108
+ name: privacyNoticeUrlKey,
109
+ englishContent: metadata.privacyNoticeUrl ?? 'Not set',
110
+ welshContent: getTranslation(
111
+ privacyNoticeUrlKey,
112
+ translations,
113
+ validation
114
+ ),
115
+ label: 'Welsh privacy notice URL'
116
+ })
117
+ } else {
118
+ rows.push({
119
+ name: privacyNoticeTextKey,
120
+ englishContent: metadata.privacyNoticeText ?? 'Not set',
121
+ welshContent: getTranslation(
122
+ privacyNoticeTextKey,
123
+ translations,
124
+ validation
125
+ ),
126
+ label: 'Welsh privacy notice text'
127
+ })
128
+ }
129
+
130
+ return /** @type {TranslationRow[]} */ (rows)
131
+ }
132
+
133
+ /**
134
+ * @import { FormMetadata } from '~/src/form/form-metadata/types.js'
135
+ * @import { FormDefinition } from '~/src/form/form-definition/types.js'
136
+ * @import { TranslationRow, ValidationFailure} from '~/src/form/form-editor/translations/translations-config.js'
137
+ */
@@ -0,0 +1,297 @@
1
+ import { ComponentType } from '~/src/components/enums.js'
2
+ import { isListType, isTypeWithInstructions } from '~/src/components/helpers.js'
3
+ import {
4
+ IGNORE_FIELDS,
5
+ TranslationRowTypes,
6
+ drillDown,
7
+ keyConfig,
8
+ lookupTranslation
9
+ } from '~/src/form/form-editor/translations/translations-config.js'
10
+ import { buildOverviewSection } from '~/src/form/form-editor/translations/translations-overview.js'
11
+ import { getListFromComponent } from '~/src/form/utils/list.js'
12
+ import { getPageNum, getQuestionNum } from '~/src/form/utils/numbering.js'
13
+ import { ControllerType } from '~/src/pages/enums.js'
14
+ import { hasComponents } from '~/src/pages/helpers.js'
15
+
16
+ /**
17
+ * Create a translation row for a single entity field.
18
+ * @param {ComponentDef | Page | Item} entity - The source entity that contains the English content.
19
+ * @param {string} keyType - The translation row type to build.
20
+ * @param {{ pageNum: number, questionNum?: number, itemNum?: number, translations: Record<string, string>, validation?: ValidationFailure<any> }} options - The row context including numbering and translation values.
21
+ * @returns {TranslationRow} The populated translation row for the entity field.
22
+ */
23
+ function createRow(
24
+ entity,
25
+ keyType,
26
+ { pageNum, questionNum, itemNum, translations, validation }
27
+ ) {
28
+ const keyProperties = keyConfig[keyType]
29
+
30
+ const innerEnglishContent = drillDown(
31
+ keyType,
32
+ entity,
33
+ keyProperties.jsonSuffix
34
+ )
35
+
36
+ const keyName = `${keyProperties.jsonPrefix}.${entity.id}.${keyProperties.jsonSuffix}`
37
+
38
+ return {
39
+ name: keyName,
40
+ type: keyType,
41
+ pageNum,
42
+ questionNum,
43
+ itemNum,
44
+ englishContent: innerEnglishContent,
45
+ welshContent:
46
+ validation?.formValues[keyName] ??
47
+ lookupTranslation(keyName, translations),
48
+ label: keyProperties.getLabel(pageNum, questionNum, itemNum)
49
+ }
50
+ }
51
+
52
+ const allowedControllerTypesSet = new Set([
53
+ ControllerType.Page,
54
+ ControllerType.Repeat,
55
+ ControllerType.Start,
56
+ ControllerType.FileUpload,
57
+ ControllerType.Terminal
58
+ ])
59
+
60
+ /**
61
+ * Build the translation rows for a single page.
62
+ * @param {Page} page - The page definition to inspect for translatable content.
63
+ * @param {number} pageNum - The page number used in the generated labels.
64
+ * @param {Record<string, string>} translations - Existing Welsh translations keyed by translation name.
65
+ * @param {ValidationFailure<any>} [validation] - Optional validation context for posted form values.
66
+ * @returns {TranslationRow[]} The translation rows generated for the page.
67
+ */
68
+ function buildPage(page, pageNum, translations, validation) {
69
+ if (page.controller && !allowedControllerTypesSet.has(page.controller)) {
70
+ return []
71
+ }
72
+
73
+ const translationRows = []
74
+ if (page.title) {
75
+ const keyProperties = keyConfig[TranslationRowTypes.PageHeading]
76
+
77
+ const keyName = `${keyProperties.jsonPrefix}.${page.id}.${keyProperties.jsonSuffix}`
78
+
79
+ translationRows.push({
80
+ name: keyName,
81
+ type: TranslationRowTypes.PageHeading,
82
+ pageNum,
83
+ englishContent: page.title,
84
+ welshContent:
85
+ validation?.formValues[keyName] ??
86
+ lookupTranslation(keyName, translations),
87
+ label: keyProperties.getLabel(pageNum)
88
+ })
89
+ }
90
+
91
+ const guidance =
92
+ hasComponents(page) && page.components[0].type === ComponentType.Markdown
93
+ ? page.components[0]
94
+ : undefined
95
+ if (guidance) {
96
+ const keyProperties = keyConfig[TranslationRowTypes.PageGuidance]
97
+ const keyName = `${keyProperties.jsonPrefix}.${guidance.id}.${keyProperties.jsonSuffix}`
98
+
99
+ translationRows.push({
100
+ name: keyName,
101
+ type: TranslationRowTypes.PageGuidance,
102
+ pageNum,
103
+ englishContent: guidance.content,
104
+ welshContent:
105
+ validation?.formValues[keyName] ??
106
+ lookupTranslation(keyName, translations),
107
+ label: keyProperties.getLabel(pageNum)
108
+ })
109
+ }
110
+
111
+ if (page.controller === ControllerType.Repeat) {
112
+ const keyProperties = keyConfig[TranslationRowTypes.RepeatTitle]
113
+ const keyName = `${keyProperties.jsonPrefix}.${page.id}.${keyProperties.jsonSuffix}`
114
+
115
+ translationRows.push({
116
+ name: keyName,
117
+ type: TranslationRowTypes.RepeatTitle,
118
+ pageNum,
119
+ englishContent: page.repeat.options.title,
120
+ welshContent:
121
+ validation?.formValues[keyName] ??
122
+ lookupTranslation(keyName, translations),
123
+ label: keyProperties.getLabel(pageNum)
124
+ })
125
+ }
126
+
127
+ return translationRows
128
+ }
129
+
130
+ /**
131
+ * Build the translation rows for a single component.
132
+ * @param {FormDefinition} definition - The overall form definition used to resolve list-based content.
133
+ * @param {ComponentDef} component - The component to inspect for translatable fields.
134
+ * @param {number} pageNum - The page number used in the generated labels.
135
+ * @param {number} questionNum - The question number used in the generated labels.
136
+ * @param {Record<string, string>} translations - Existing Welsh translations keyed by translation name.
137
+ * @param {ValidationFailure<any>} [validation] - Optional validation context for posted form values.
138
+ * @returns {TranslationRow[]} The translation rows generated for the component.
139
+ */
140
+ function buildComponent(
141
+ definition,
142
+ component,
143
+ pageNum,
144
+ questionNum,
145
+ translations,
146
+ validation
147
+ ) {
148
+ if (IGNORE_FIELDS.includes(component.type)) {
149
+ return []
150
+ }
151
+
152
+ const rows = []
153
+
154
+ const options = { pageNum, questionNum, translations, validation }
155
+
156
+ const typed = /** @type {InputFieldsComponentsDef} */ (component)
157
+ if (typed.title && component.type !== ComponentType.PaymentField) {
158
+ rows.push(createRow(component, TranslationRowTypes.QuestionText, options))
159
+ }
160
+ if (typed.hint) {
161
+ rows.push(createRow(component, TranslationRowTypes.QuestionHint, options))
162
+ }
163
+
164
+ if (component.type !== ComponentType.PaymentField) {
165
+ rows.push(
166
+ createRow(component, TranslationRowTypes.ShortDescription, options)
167
+ )
168
+ } else {
169
+ rows.push(
170
+ createRow(component, TranslationRowTypes.PaymentDescription, options)
171
+ )
172
+ }
173
+
174
+ if (component.type === ComponentType.DeclarationField) {
175
+ rows.push(
176
+ createRow(component, TranslationRowTypes.DeclarationBody, options)
177
+ )
178
+ }
179
+
180
+ if (component.type === ComponentType.PaymentField) {
181
+ rows.push(
182
+ createRow(component, TranslationRowTypes.PaymentDescription, options)
183
+ )
184
+ }
185
+
186
+ if (typed.options.instructionText && isTypeWithInstructions(component.type)) {
187
+ rows.push(
188
+ createRow(component, TranslationRowTypes.InstructionText, options)
189
+ )
190
+ }
191
+
192
+ if (isListType(component.type)) {
193
+ addSelectionOptions(rows, component, definition, options)
194
+ }
195
+ return rows
196
+ }
197
+
198
+ /**
199
+ * Add translation rows for the select item options of a list-based component.
200
+ * @param {any[]} rows - The accumulator for translation rows.
201
+ * @param {ComponentDef} component - The list-based component to inspect.
202
+ * @param {FormDefinition} definition - The form definition that contains the option list data.
203
+ * @param {{ pageNum: number, questionNum?: number, translations: Record<string, string>, validation?: ValidationFailure<any> }} options - The row context including numbering and translation values.
204
+ */
205
+ function addSelectionOptions(rows, component, definition, options) {
206
+ // Temporary workaround - ignore for Yes/No and use defaults in the plugin,
207
+ // until we create a solution that allows users to override the Yes/No options
208
+ if (component.type === ComponentType.YesNoField) {
209
+ return
210
+ }
211
+
212
+ const list = getListFromComponent(component, definition)
213
+ if (list?.items.length) {
214
+ let itemNum = 0
215
+ for (const item of list.items) {
216
+ itemNum++
217
+ const listOptions = {
218
+ ...options,
219
+ itemNum
220
+ }
221
+ if (item.hint) {
222
+ rows.push(
223
+ createRow(item, TranslationRowTypes.ListItemText, listOptions),
224
+ createRow(item, TranslationRowTypes.ListItemHint, listOptions)
225
+ )
226
+ } else {
227
+ rows.push(
228
+ createRow(item, TranslationRowTypes.ListItemText, listOptions)
229
+ )
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Build the overview and form translation rows for a form definition.
237
+ * @param {FormMetadata} metadata - The form metadata used to build the overview section.
238
+ * @param {FormDefinition} definition - The form definition containing pages and components.
239
+ * @param {ValidationFailure<any>} [validation] - Optional validation context for posted form values.
240
+ * @returns {{ overviewRows: TranslationRow[], formRows: TranslationRow[] }} The generated overview and form translation rows.
241
+ */
242
+ export function buildTranslationDataRows(metadata, definition, validation) {
243
+ const translationsJSON = /** @type {Record<string, string>} */ (
244
+ // @ts-expect-error - dynamic language definition
245
+ definition.metadata?.translations?.cy
246
+ )
247
+
248
+ const overviewRows = buildOverviewSection(
249
+ metadata,
250
+ definition,
251
+ translationsJSON,
252
+ validation
253
+ )
254
+
255
+ const formRows = []
256
+ for (const page of definition.pages) {
257
+ const pageId = /** @type {string} */ (page.id)
258
+ const pageNum = getPageNum(definition, pageId)
259
+ const components = hasComponents(page) ? page.components : []
260
+ if (components.length === 0) {
261
+ continue
262
+ }
263
+
264
+ formRows.push(...buildPage(page, pageNum, translationsJSON, validation))
265
+
266
+ for (const component of components) {
267
+ if (component.type === ComponentType.Markdown) {
268
+ continue
269
+ }
270
+ const questionId = /** @type {string} */ (component.id)
271
+ const questionNum = getQuestionNum(definition, pageId, questionId)
272
+
273
+ formRows.push(
274
+ ...buildComponent(
275
+ definition,
276
+ component,
277
+ pageNum,
278
+ questionNum,
279
+ translationsJSON,
280
+ validation
281
+ )
282
+ )
283
+ }
284
+ }
285
+
286
+ return {
287
+ overviewRows,
288
+ formRows
289
+ }
290
+ }
291
+
292
+ /**
293
+ * @import { ComponentDef, InputFieldsComponentsDef } from '~/src/components/types.js'
294
+ * @import { FormMetadata } from '~/src/form/form-metadata/types.js'
295
+ * @import { FormDefinition, Item, Page } from '~/src/form/form-definition/types.js'
296
+ * @import { TranslationRow, ValidationFailure } from '~/src/form/form-editor/translations/translations-config.js'
297
+ */
@@ -78,6 +78,13 @@ export const formSubmitPayloadSchema = Joi.object<SubmitPayload>()
78
78
  .optional()
79
79
  .allow('')
80
80
  .description('Reference number for the form submission'),
81
+ language: Joi.string()
82
+ // TODO - make this required in later releases - optional for now to avoid release glitches
83
+ .optional()
84
+ .allow('')
85
+ .description(
86
+ 'The currently-selected language at point of form submission'
87
+ ),
81
88
  main: Joi.array<SubmitRecord>()
82
89
  .items(formSubmitRecordSchema)
83
90
  .required()
@@ -74,6 +74,11 @@ export interface SubmitPayload {
74
74
  */
75
75
  referenceNumber?: string
76
76
 
77
+ /**
78
+ * The currently-selected language at the point of submission
79
+ */
80
+ language?: string
81
+
77
82
  /**
78
83
  * The main form anwsers
79
84
  */
@@ -1,4 +1,10 @@
1
- import { type ListComponentsDef } from '~/src/components/types.js'
1
+ import { ComponentType } from '~/src/components/enums.js'
2
+ import { hasListField } from '~/src/components/helpers.js'
3
+ import {
4
+ type ComponentDef,
5
+ type ListComponentsDef
6
+ } from '~/src/components/types.js'
7
+ import { getYesNoList } from '~/src/components/yes-no-helper.js'
2
8
  import {
3
9
  type FormDefinition,
4
10
  type List
@@ -23,3 +29,30 @@ export function findDefinitionListFromComponent(
23
29
 
24
30
  return list
25
31
  }
32
+
33
+ /**
34
+ * Finds the list in the component, if it exists. Handles a Yes/No list which doesn't link to the list in the normal way
35
+ * @param { ComponentDef | undefined } component
36
+ * @param {FormDefinition} definition
37
+ * @returns { List | undefined }
38
+ */
39
+ export function getListFromComponent(
40
+ component: ComponentDef | undefined,
41
+ definition: FormDefinition
42
+ ) {
43
+ if (!component) {
44
+ return undefined
45
+ }
46
+
47
+ if (component.type === ComponentType.YesNoField) {
48
+ return getYesNoList()
49
+ }
50
+
51
+ const listId = hasListField(component) ? component.list : undefined
52
+
53
+ if (listId) {
54
+ return definition.lists.find((list) => list.id === listId)
55
+ }
56
+
57
+ return undefined
58
+ }