@defra/forms-engine-plugin 5.0.0-alpha.7 → 5.0.0-alpha.9

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 (178) hide show
  1. package/.public/javascripts/application.min.js.map +1 -1
  2. package/.public/javascripts/shared.min.js +1 -1
  3. package/.public/javascripts/shared.min.js.map +1 -1
  4. package/.public/stylesheets/application.min.css.map +1 -1
  5. package/.server/client/javascripts/maps.d.ts +5 -0
  6. package/.server/client/javascripts/maps.js +6 -0
  7. package/.server/client/javascripts/maps.js.map +1 -0
  8. package/.server/client/javascripts/shared.d.ts +1 -4
  9. package/.server/client/javascripts/shared.js +1 -4
  10. package/.server/client/javascripts/shared.js.map +1 -1
  11. package/.server/client/stylesheets/maps.scss +4 -0
  12. package/.server/client/stylesheets/shared.scss +1 -5
  13. package/.server/server/constants.d.ts +1 -0
  14. package/.server/server/constants.js +2 -1
  15. package/.server/server/constants.js.map +1 -1
  16. package/.server/server/plugins/engine/components/ComponentCollection.js +10 -6
  17. package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
  18. package/.server/server/plugins/engine/components/DatePartsField.d.ts +4 -2
  19. package/.server/server/plugins/engine/components/DatePartsField.js +14 -11
  20. package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
  21. package/.server/server/plugins/engine/components/DeclarationField.d.ts +4 -2
  22. package/.server/server/plugins/engine/components/DeclarationField.js +5 -3
  23. package/.server/server/plugins/engine/components/DeclarationField.js.map +1 -1
  24. package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +24 -2
  25. package/.server/server/plugins/engine/components/EastingNorthingField.js +93 -52
  26. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
  27. package/.server/server/plugins/engine/components/FormComponent.d.ts +4 -1
  28. package/.server/server/plugins/engine/components/FormComponent.js +3 -0
  29. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  30. package/.server/server/plugins/engine/components/LatLongField.d.ts +24 -2
  31. package/.server/server/plugins/engine/components/LatLongField.js +85 -46
  32. package/.server/server/plugins/engine/components/LatLongField.js.map +1 -1
  33. package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +3 -2
  34. package/.server/server/plugins/engine/components/LocationFieldHelpers.js +10 -11
  35. package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -1
  36. package/.server/server/plugins/engine/components/MonthYearField.d.ts +4 -2
  37. package/.server/server/plugins/engine/components/MonthYearField.js +16 -13
  38. package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
  39. package/.server/server/plugins/engine/components/PaymentField.d.ts +1 -0
  40. package/.server/server/plugins/engine/components/PaymentField.js +9 -5
  41. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
  42. package/.server/server/plugins/engine/components/UkAddressField.js +22 -7
  43. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  44. package/.server/server/plugins/engine/components/YesNoField.d.ts +3 -2
  45. package/.server/server/plugins/engine/components/YesNoField.js +5 -3
  46. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  47. package/.server/server/plugins/engine/components/helpers/components.js +1 -2
  48. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  49. package/.server/server/plugins/engine/components/helpers/index.d.ts +7 -0
  50. package/.server/server/plugins/engine/components/helpers/index.js +13 -1
  51. package/.server/server/plugins/engine/components/helpers/index.js.map +1 -1
  52. package/.server/server/plugins/engine/helpers.d.ts +21 -2
  53. package/.server/server/plugins/engine/helpers.js +43 -5
  54. package/.server/server/plugins/engine/helpers.js.map +1 -1
  55. package/.server/server/plugins/engine/i18n/createFormTranslator.d.ts +4 -4
  56. package/.server/server/plugins/engine/i18n/createFormTranslator.js +27 -5
  57. package/.server/server/plugins/engine/i18n/createFormTranslator.js.map +1 -1
  58. package/.server/server/plugins/engine/i18n/extractBaseTranslations.d.ts +1 -1
  59. package/.server/server/plugins/engine/i18n/extractBaseTranslations.js +9 -0
  60. package/.server/server/plugins/engine/i18n/extractBaseTranslations.js.map +1 -1
  61. package/.server/server/plugins/engine/i18n/form.d.ts +13 -0
  62. package/.server/server/plugins/engine/i18n/form.js +35 -0
  63. package/.server/server/plugins/engine/i18n/form.js.map +1 -0
  64. package/.server/server/plugins/engine/i18n/languages.d.ts +10 -0
  65. package/.server/server/plugins/engine/i18n/languages.js +19 -0
  66. package/.server/server/plugins/engine/i18n/languages.js.map +1 -0
  67. package/.server/server/plugins/engine/i18n/translations/cy.json +3 -1
  68. package/.server/server/plugins/engine/i18n/translations/en-GB.json +3 -1
  69. package/.server/server/plugins/engine/index.d.ts +3 -0
  70. package/.server/server/plugins/engine/index.js +3 -0
  71. package/.server/server/plugins/engine/index.js.map +1 -1
  72. package/.server/server/plugins/engine/models/FormModel.d.ts +0 -4
  73. package/.server/server/plugins/engine/models/FormModel.js +9 -15
  74. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  75. package/.server/server/plugins/engine/models/SummaryViewModel.js +2 -1
  76. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  77. package/.server/server/plugins/engine/outputFormatters/human/v1.js +2 -1
  78. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  79. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +2 -0
  80. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +2 -0
  81. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  82. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +1 -1
  83. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -2
  84. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  85. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -1
  86. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -2
  87. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  88. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +6 -3
  89. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  90. package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +1 -1
  91. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +3 -3
  92. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
  93. package/.server/server/plugins/engine/types/schema.js +1 -0
  94. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  95. package/.server/server/plugins/engine/types.d.ts +1 -0
  96. package/.server/server/plugins/engine/types.js.map +1 -1
  97. package/.server/server/plugins/engine/views/components/ukaddressfield.html +1 -1
  98. package/.server/server/plugins/nunjucks/filters/applyUrlParam.d.ts +9 -0
  99. package/.server/server/plugins/nunjucks/filters/applyUrlParam.js +17 -0
  100. package/.server/server/plugins/nunjucks/filters/applyUrlParam.js.map +1 -0
  101. package/.server/server/plugins/nunjucks/filters/applyUrlParam.test.js +17 -0
  102. package/.server/server/plugins/nunjucks/filters/applyUrlParam.test.js.map +1 -0
  103. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  104. package/.server/server/plugins/nunjucks/filters/index.d.ts +1 -0
  105. package/.server/server/plugins/nunjucks/filters/index.js +1 -0
  106. package/.server/server/plugins/nunjucks/filters/index.js.map +1 -1
  107. package/.server/server/plugins/payment/helper.d.ts +11 -1
  108. package/.server/server/plugins/payment/helper.js +18 -2
  109. package/.server/server/plugins/payment/helper.js.map +1 -1
  110. package/.server/server/plugins/payment/helper.test.js +2 -1
  111. package/.server/server/plugins/payment/helper.test.js.map +1 -1
  112. package/.server/server/plugins/payment/service.d.ts +2 -1
  113. package/.server/server/plugins/payment/service.js +5 -2
  114. package/.server/server/plugins/payment/service.js.map +1 -1
  115. package/.server/server/plugins/payment/service.test.js +7 -3
  116. package/.server/server/plugins/payment/service.test.js.map +1 -1
  117. package/.server/server/plugins/payment/types.d.ts +4 -0
  118. package/.server/server/plugins/payment/types.js +1 -0
  119. package/.server/server/plugins/payment/types.js.map +1 -1
  120. package/.server/server/plugins/postcode-lookup/models/index.d.ts +247 -3
  121. package/.server/server/plugins/postcode-lookup/models/index.js +86 -33
  122. package/.server/server/plugins/postcode-lookup/models/index.js.map +1 -1
  123. package/.server/server/plugins/postcode-lookup/routes/index.d.ts +15 -1
  124. package/.server/server/plugins/postcode-lookup/routes/index.js +47 -15
  125. package/.server/server/plugins/postcode-lookup/routes/index.js.map +1 -1
  126. package/.server/server/plugins/postcode-lookup/routes/index.test.js +84 -0
  127. package/.server/server/plugins/postcode-lookup/routes/index.test.js.map +1 -0
  128. package/.server/server/plugins/postcode-lookup/types.d.ts +12 -0
  129. package/.server/server/plugins/postcode-lookup/types.js +6 -1
  130. package/.server/server/plugins/postcode-lookup/types.js.map +1 -1
  131. package/.server/server/plugins/postcode-lookup/views/postcode-lookup-details.html +3 -3
  132. package/package.json +7 -1
  133. package/src/client/javascripts/maps.js +5 -0
  134. package/src/client/javascripts/shared.js +1 -4
  135. package/src/client/stylesheets/maps.scss +4 -0
  136. package/src/client/stylesheets/shared.scss +1 -5
  137. package/src/server/constants.js +2 -1
  138. package/src/server/plugins/engine/components/ComponentCollection.ts +15 -9
  139. package/src/server/plugins/engine/components/DatePartsField.ts +16 -12
  140. package/src/server/plugins/engine/components/DeclarationField.ts +6 -4
  141. package/src/server/plugins/engine/components/EastingNorthingField.ts +156 -88
  142. package/src/server/plugins/engine/components/FormComponent.ts +4 -1
  143. package/src/server/plugins/engine/components/LatLongField.ts +129 -57
  144. package/src/server/plugins/engine/components/LocationFieldHelpers.ts +14 -11
  145. package/src/server/plugins/engine/components/MonthYearField.ts +16 -11
  146. package/src/server/plugins/engine/components/PaymentField.ts +9 -4
  147. package/src/server/plugins/engine/components/UkAddressField.ts +29 -6
  148. package/src/server/plugins/engine/components/YesNoField.ts +6 -5
  149. package/src/server/plugins/engine/components/helpers/components.ts +1 -2
  150. package/src/server/plugins/engine/components/helpers/index.ts +19 -0
  151. package/src/server/plugins/engine/helpers.ts +51 -5
  152. package/src/server/plugins/engine/i18n/createFormTranslator.ts +43 -12
  153. package/src/server/plugins/engine/i18n/extractBaseTranslations.ts +7 -1
  154. package/src/server/plugins/engine/i18n/form.ts +54 -0
  155. package/src/server/plugins/engine/i18n/languages.ts +19 -0
  156. package/src/server/plugins/engine/i18n/translations/cy.json +3 -1
  157. package/src/server/plugins/engine/i18n/translations/en-GB.json +3 -1
  158. package/src/server/plugins/engine/index.ts +6 -0
  159. package/src/server/plugins/engine/models/FormModel.ts +9 -15
  160. package/src/server/plugins/engine/models/SummaryViewModel.ts +2 -1
  161. package/src/server/plugins/engine/outputFormatters/human/v1.ts +2 -1
  162. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +3 -0
  163. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +2 -2
  164. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -2
  165. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +9 -3
  166. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +6 -3
  167. package/src/server/plugins/engine/types/schema.ts +1 -0
  168. package/src/server/plugins/engine/types.ts +1 -0
  169. package/src/server/plugins/engine/views/components/ukaddressfield.html +1 -1
  170. package/src/server/plugins/nunjucks/filters/applyUrlParam.js +17 -0
  171. package/src/server/plugins/nunjucks/filters/index.js +1 -0
  172. package/src/server/plugins/payment/helper.js +22 -2
  173. package/src/server/plugins/payment/service.js +6 -2
  174. package/src/server/plugins/payment/types.js +1 -0
  175. package/src/server/plugins/postcode-lookup/models/index.js +75 -43
  176. package/src/server/plugins/postcode-lookup/routes/index.js +73 -16
  177. package/src/server/plugins/postcode-lookup/types.js +6 -1
  178. package/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +3 -3
@@ -114,7 +114,7 @@ export class SummaryPageController extends QuestionPageController {
114
114
  viewModel.paymentDetails = this.buildPaymentDetails(
115
115
  paymentField,
116
116
  paymentState,
117
- t
117
+ translator
118
118
  )
119
119
  }
120
120
 
@@ -151,8 +151,9 @@ export class SummaryPageController extends QuestionPageController {
151
151
  paymentState: NonNullable<
152
152
  ReturnType<PaymentField['getPaymentStateFromState']>
153
153
  >,
154
- t: (key: string, opts?: Record<string, unknown>) => string
154
+ translator: Translator
155
155
  ) {
156
+ const { t } = translator
156
157
  const rows = [
157
158
  {
158
159
  key: { text: t('pages.summary.paymentFor') },
@@ -171,7 +172,12 @@ export class SummaryPageController extends QuestionPageController {
171
172
  if (paymentState.preAuth?.createdAt) {
172
173
  rows.push({
173
174
  key: { text: t('pages.summary.dateOfPayment') },
174
- value: { text: formatPaymentDate(paymentState.preAuth.createdAt) }
175
+ value: {
176
+ text: formatPaymentDate(
177
+ paymentState.preAuth.createdAt,
178
+ translator.language
179
+ )
180
+ }
175
181
  })
176
182
  }
177
183
 
@@ -36,7 +36,7 @@ export function buildMainRecords(
36
36
 
37
37
  for (const item of fieldItems) {
38
38
  if (item.field instanceof PaymentField) {
39
- records.push(...buildPaymentRecords(item))
39
+ records.push(...buildPaymentRecords(item, translator))
40
40
  } else if (item.field instanceof GeospatialField) {
41
41
  // Stringify of GeoJSON is done here rather than inside `getContextValueFromState`
42
42
  // so we don't incur the overhead of JSON.stringify on every request when building context
@@ -67,7 +67,10 @@ export function buildMainRecords(
67
67
  *
68
68
  * Returns an empty array if no payment state exists.
69
69
  */
70
- export function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {
70
+ export function buildPaymentRecords(
71
+ item: DetailItemField,
72
+ translator: Translator
73
+ ): SubmitRecord[] {
71
74
  const paymentState = (item.field as PaymentField).getPaymentStateFromState(
72
75
  item.state
73
76
  )
@@ -96,7 +99,7 @@ export function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {
96
99
  name: `${item.name}_paymentDate`,
97
100
  title: 'Payment date',
98
101
  value: paymentState.preAuth?.createdAt
99
- ? formatPaymentDate(paymentState.preAuth.createdAt)
102
+ ? formatPaymentDate(paymentState.preAuth.createdAt, translator.language)
100
103
  : ''
101
104
  }
102
105
  ]
@@ -50,6 +50,7 @@ export const formAdapterSubmissionMessageDataSchema =
50
50
  reference: Joi.string().required(),
51
51
  amount: Joi.number().required(),
52
52
  description: Joi.string().required(),
53
+ descriptionInEng: Joi.string().optional(),
53
54
  createdAt: Joi.string().required()
54
55
  })
55
56
  .optional(),
@@ -558,6 +558,7 @@ export interface FormAdapterPayment {
558
558
  reference: string
559
559
  amount: number
560
560
  description: string
561
+ descriptionInEng?: string
561
562
  createdAt: string
562
563
  }
563
564
 
@@ -61,7 +61,7 @@
61
61
  },
62
62
  classes: "govuk-button--secondary govuk-!-margin-right-1 govuk-!-margin-bottom-0"
63
63
  }) }}
64
- <p class="govuk-body govuk-!-margin-bottom-0">or <button class="govuk-link govuk-button--link govuk-!-margin-right-1 govuk-!-margin-bottom-0" name="action" value="external-{{component.model.name}}--step:manual">{{ t('components.addressField.enterManually') }}</button></p>
64
+ <p class="govuk-body govuk-!-margin-bottom-0">{{ t('common.or')}} <button class="govuk-link govuk-button--link govuk-!-margin-right-1 govuk-!-margin-bottom-0" name="action" value="external-{{component.model.name}}--step:manual">{{ t('components.addressField.enterManually') }}</button></p>
65
65
  </div>
66
66
  {# Include a line break if this is the last component #}
67
67
  {% if components[components.length - 1] == component %}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Nunjucks filter to add/replace a parameter on a query string of a url.
3
+ * Currently only used in forms-runner.
4
+ * @param {string} urlPath - existing relative url with query string
5
+ * @param {string} paramName - name of parameter
6
+ * @param {string} paramValue - value of parameter
7
+ * @returns {string}
8
+ */
9
+ export function applyUrlParam(urlPath, paramName, paramValue) {
10
+ if (typeof urlPath !== 'string') {
11
+ return ''
12
+ }
13
+ const url = new URL(urlPath, 'https://dummy')
14
+ url.searchParams.set(paramName, paramValue)
15
+
16
+ return url.pathname + url.search
17
+ }
@@ -6,3 +6,4 @@ export { href } from './href.js'
6
6
  export { field } from './field.js'
7
7
  export { page } from './page.js'
8
8
  export { merge } from './merge.js'
9
+ export { applyUrlParam } from './applyUrlParam.js'
@@ -1,5 +1,6 @@
1
1
  import { format } from 'date-fns'
2
2
 
3
+ import { toLanguageLocale } from '../engine/helpers.js'
3
4
  import { PaymentService } from './service.js'
4
5
 
5
6
  export const DEFAULT_PAYMENT_HELP_URL =
@@ -28,10 +29,29 @@ export async function createPaymentService(
28
29
  /**
29
30
  * Formats a payment date for display
30
31
  * @param {string} isoString - ISO date string
32
+ * @param {string} language - the target language
31
33
  * @returns {string} Formatted date string (e.g., "26 January 2026 5:01pm")
32
34
  */
33
- export function formatPaymentDate(isoString) {
34
- return format(new Date(isoString), 'd MMMM yyyy h:mmaaa')
35
+ export function formatPaymentDate(isoString, language) {
36
+ return formatDateForLanguage(
37
+ new Date(isoString),
38
+ 'd MMMM yyyy h:mmaaa',
39
+ language
40
+ )
41
+ }
42
+
43
+ /**
44
+ * Formats a date for display in a specific language.
45
+ * To use additional languages, the appropriate module must be imported from 'date-fns/locale'
46
+ * @param { Date | string } date - the date
47
+ * @param {string} formatMask - the format mask
48
+ * @param {string} language - the target language
49
+ * @returns {string} Formatted date string (e.g., "26 January 2026 5:01pm")
50
+ */
51
+ export function formatDateForLanguage(date, formatMask, language) {
52
+ return format(date, formatMask, {
53
+ locale: toLanguageLocale(language)
54
+ })
35
55
  }
36
56
 
37
57
  /**
@@ -1,6 +1,7 @@
1
1
  import { StatusCodes } from 'http-status-codes'
2
2
 
3
3
  import { logger } from '../../common/helpers/logging/logger.js'
4
+ import { toPayLanguageCode } from '../engine/helpers.js'
4
5
  import {
5
6
  buildPaymentInfo,
6
7
  convertPenceToPounds
@@ -38,7 +39,7 @@ export class PaymentService {
38
39
  * @param {string} returnUrl
39
40
  * @param {string} reference
40
41
  * @param {boolean} isLivePayment
41
- * @param {{ formId: string, slug: string } | undefined } metadata
42
+ * @param {{ formId: string, slug: string, language: string } | undefined } metadata
42
43
  * @param {string} [email] - optional email to prepopulate on GOV.UK Pay
43
44
  */
44
45
  async createPayment(
@@ -50,6 +51,8 @@ export class PaymentService {
50
51
  metadata,
51
52
  email
52
53
  ) {
54
+ const language = toPayLanguageCode(metadata?.language)
55
+
53
56
  try {
54
57
  /** @type {CreatePaymentRequest} */
55
58
  const payload = {
@@ -58,7 +61,8 @@ export class PaymentService {
58
61
  reference,
59
62
  metadata,
60
63
  return_url: returnUrl,
61
- delayed_capture: true
64
+ delayed_capture: true,
65
+ language
62
66
  }
63
67
 
64
68
  // Prepopulate email on GOV.UK Pay if provided
@@ -18,6 +18,7 @@
18
18
  * @property {string} reference - Unique reference for the payment
19
19
  * @property {string} description - Human-readable description of the payment
20
20
  * @property {string} return_url - URL to redirect the user to after payment
21
+ * @property {string} language - the desired language for translations
21
22
  * @property {boolean} [delayed_capture] - Whether to delay capturing the payment
22
23
  * @property {{ formId: string, slug: string }} [metadata] - Additional metadata for the payment
23
24
  * @property {string} [email] - Email to prepopulate on GOV.UK Pay (max 254 chars)
@@ -16,6 +16,7 @@ const countyFieldName = 'county'
16
16
  const postcodeFieldName = 'postcode'
17
17
 
18
18
  const GOVUK_MARGIN_RIGHT_1 = 'govuk-!-margin-right-1'
19
+ const COMMON_BACK = 'common.back'
19
20
 
20
21
  export const steps = {
21
22
  // Step 1: Postcode/building name input
@@ -403,19 +404,20 @@ function getHref(step) {
403
404
  /**
404
405
  * The postcode lookup details form view model
405
406
  * @param {PostcodeLookupSessionData} data
407
+ * @param {Translator} translator
406
408
  * @param {PostcodeLookupDetailsData} [payload]
407
409
  * @param {Error} [err]
408
410
  */
409
- export function detailsViewModel(data, payload, err) {
410
- const {
411
- componentTitle: pageTitle,
412
- formName,
413
- sourceUrl,
414
- language = 'en-GB'
415
- } = data.initial
411
+ export function detailsViewModel(data, translator, payload, err) {
412
+ const { componentId, componentHint, sourceUrl, languages } = data.initial
413
+
414
+ const { t, tComponent, tForm } = translator
415
+
416
+ const component = /** @type {ComponentDef} */ ({ id: componentId })
416
417
 
417
418
  const backLink = {
418
- href: sourceUrl
419
+ href: sourceUrl,
420
+ text: t(COMMON_BACK)
419
421
  }
420
422
 
421
423
  const { errors, postcodeQueryError, buildingNameQueryError } =
@@ -425,42 +427,54 @@ export function detailsViewModel(data, payload, err) {
425
427
  payload ?? data.details,
426
428
  postcodeQueryError,
427
429
  buildingNameQueryError,
428
- language
430
+ translator.language
429
431
  )
430
432
 
433
+ const hint = componentHint && {
434
+ text: tComponent(component, 'hint')
435
+ }
436
+
431
437
  const continueButton = {
432
- text: i18nT('postcodeLookup.findAddress', language),
438
+ text: t('postcodeLookup.findAddress'),
433
439
  classes: GOVUK_MARGIN_RIGHT_1
434
440
  }
435
441
  const manualLink = {
436
- text: i18nT('postcodeLookup.enterManually', language),
442
+ text: t('postcodeLookup.enterManually'),
437
443
  href: getHref(steps.manual)
438
444
  }
439
445
 
446
+ const selector = languages?.length ? { languages } : {}
447
+
440
448
  return {
441
449
  step: steps.details,
442
450
  showTitle: true,
443
- name: formName,
451
+ name: tForm('title'),
444
452
  serviceUrl: sourceUrl,
445
- pageTitle,
453
+ pageTitle: tComponent(component, 'title'),
454
+ hint,
446
455
  backLink,
447
456
  errors,
448
457
  fields,
449
- buttons: { continueButton, manualLink }
458
+ buttons: { continueButton, manualLink },
459
+ t,
460
+ language: translator.language,
461
+ ...selector
450
462
  }
451
463
  }
452
464
 
453
465
  /**
454
466
  * The postcode lookup select form view model
455
467
  * @param {{ session: PostcodeLookupSessionData, apiKey: string }} data
468
+ * @param {Translator} translator
456
469
  * @param {PostcodeLookupSelectPayload} [payload]
457
470
  * @param {Error} [err]
458
471
  */
459
- export async function selectViewModel(data, payload, err) {
472
+ export async function selectViewModel(data, translator, payload, err) {
460
473
  const { session, apiKey } = data
461
474
  const { details, initial } = session
462
475
  const { postcodeQuery, buildingNameQuery } = details
463
- const language = initial.language ?? 'en-GB'
476
+ const language = translator.language
477
+ const { t, tComponent, tForm } = translator
464
478
 
465
479
  const {
466
480
  hasAddresses,
@@ -471,12 +485,18 @@ export async function selectViewModel(data, payload, err) {
471
485
  } = await getAddresses(postcodeQuery, buildingNameQuery, apiKey)
472
486
 
473
487
  const title = hasAddresses
474
- ? initial.componentTitle
475
- : i18nT('postcodeLookup.noAddressFoundTitle', language)
488
+ ? tComponent(
489
+ /** @type {ComponentDef} */ ({ id: initial.componentId }),
490
+ 'title'
491
+ )
492
+ : t('postcodeLookup.noAddressFoundTitle')
476
493
  const formPath = initial.sourceUrl
477
494
  const href = getHref()
478
495
 
479
- const backLink = { href }
496
+ const backLink = {
497
+ href,
498
+ text: t(COMMON_BACK)
499
+ }
480
500
 
481
501
  const { errors, uprnError } = buildErrors(err)
482
502
 
@@ -490,7 +510,7 @@ export async function selectViewModel(data, payload, err) {
490
510
  language
491
511
  )
492
512
 
493
- const searchAgainText = i18nT('postcodeLookup.searchAgain', language)
513
+ const searchAgainText = t('postcodeLookup.searchAgain')
494
514
 
495
515
  const searchAgainLink = {
496
516
  text: searchAgainText,
@@ -499,27 +519,29 @@ export async function selectViewModel(data, payload, err) {
499
519
 
500
520
  const continueButton = {
501
521
  href: hasAddresses ? undefined : href,
502
- text: hasAddresses
503
- ? i18nT('postcodeLookup.useThisAddress', language)
504
- : searchAgainText,
522
+ text: hasAddresses ? t('postcodeLookup.useThisAddress') : searchAgainText,
505
523
  classes: GOVUK_MARGIN_RIGHT_1
506
524
  }
507
525
  const manualLink = {
508
- text: i18nT('postcodeLookup.enterManually', language),
526
+ text: t('postcodeLookup.enterManually'),
509
527
  href: `${href}?step=${steps.manual}`
510
528
  }
511
529
 
512
530
  const addressesFoundText = hasAddresses
513
- ? i18nT('postcodeLookup.addressFound', language, { count: addressCount })
531
+ ? t('postcodeLookup.addressFound', { count: addressCount })
514
532
  : undefined
515
533
  const noAddressFoundText = !hasAddresses
516
- ? i18nT('postcodeLookup.noAddressFoundBody', language)
534
+ ? t('postcodeLookup.noAddressFoundBody')
517
535
  : undefined
518
536
 
537
+ const selector = initial.languages?.length
538
+ ? { languages: initial.languages }
539
+ : {}
540
+
519
541
  return {
520
542
  step: steps.select,
521
543
  showTitle: true,
522
- name: title,
544
+ name: tForm('title'),
523
545
  serviceUrl: formPath,
524
546
  pageTitle: title,
525
547
  backLink,
@@ -533,28 +555,31 @@ export async function selectViewModel(data, payload, err) {
533
555
  hasMultipleAddresses,
534
556
  addressesFoundText,
535
557
  noAddressFoundText,
536
- buttons: { continueButton, manualLink }
558
+ buttons: { continueButton, manualLink },
559
+ language,
560
+ ...selector
537
561
  }
538
562
  }
539
563
 
540
564
  /**
541
565
  * The postcode lookup manual form view model
542
566
  * @param {PostcodeLookupSessionData} data
567
+ * @param {Translator} translator
543
568
  * @param {PostcodeLookupManualPayload} [payload]
544
569
  * @param {Error} [err]
545
570
  */
546
- export function manualViewModel(data, payload, err) {
547
- const {
548
- componentTitle,
549
- sourceUrl,
550
- componentHint,
551
- language = 'en-GB'
552
- } = data.initial
571
+ export function manualViewModel(data, translator, payload, err) {
572
+ const { componentId, sourceUrl, componentHint, languages } = data.initial
553
573
  const formPath = sourceUrl
554
574
  const href = getHref()
555
575
 
576
+ const { t, tComponent, tForm } = translator
577
+
578
+ const component = /** @type {ComponentDef} */ ({ id: componentId })
579
+
556
580
  const backLink = {
557
- href
581
+ href,
582
+ text: t(COMMON_BACK)
558
583
  }
559
584
 
560
585
  const {
@@ -567,7 +592,7 @@ export function manualViewModel(data, payload, err) {
567
592
  } = buildErrors(err)
568
593
 
569
594
  const hint = componentHint && {
570
- text: componentHint
595
+ text: tComponent(component, 'hint')
571
596
  }
572
597
 
573
598
  const fields = getManualFields(
@@ -577,29 +602,34 @@ export function manualViewModel(data, payload, err) {
577
602
  townError,
578
603
  countyError,
579
604
  postcodeError,
580
- language
605
+ translator.language
581
606
  )
582
607
 
583
608
  const continueButton = {
584
- text: i18nT('postcodeLookup.useThisAddress', language),
609
+ text: t('postcodeLookup.useThisAddress'),
585
610
  classes: GOVUK_MARGIN_RIGHT_1
586
611
  }
587
612
  const detailsLink = {
588
- text: i18nT('postcodeLookup.findAnAddressInstead', language),
613
+ text: t('postcodeLookup.findAnAddressInstead'),
589
614
  href
590
615
  }
591
616
 
617
+ const selector = languages?.length ? { languages } : {}
618
+
592
619
  return {
593
620
  step: steps.manual,
594
621
  showTitle: true,
595
- name: componentTitle,
622
+ name: tForm('title'),
596
623
  serviceUrl: formPath,
597
- pageTitle: componentTitle,
624
+ pageTitle: tComponent(component, 'title'),
598
625
  backLink,
599
626
  errors,
600
627
  hint,
601
628
  fields,
602
- buttons: { continueButton, detailsLink }
629
+ buttons: { continueButton, detailsLink },
630
+ t,
631
+ language: translator.language,
632
+ ...selector
603
633
  }
604
634
  }
605
635
 
@@ -607,5 +637,7 @@ export function manualViewModel(data, payload, err) {
607
637
 
608
638
  /**
609
639
  * @import { ObjectSchema, ValidationErrorItem } from 'joi'
640
+ * @import { ComponentDef } from '@defra/forms-model'
610
641
  * @import { Address, PostcodeLookupDetailsData, PostcodeLookupDetailsPayload, PostcodeLookupManualPayload, PostcodeLookupSelectPayload, PostcodeLookupSessionData } from '../types.js'
642
+ * @import { Translator } from '../../engine/i18n/types.js'
611
643
  */
@@ -3,6 +3,7 @@ import { StatusCodes } from 'http-status-codes'
3
3
  import Joi from 'joi'
4
4
 
5
5
  import { EXTERNAL_STATE_APPENDAGE } from '../../../constants.js'
6
+ import { getCachedPluginTranslator } from '../../engine/i18n/form.js'
6
7
  import {
7
8
  JOURNEY_BASE_URL,
8
9
  createDetailsPayloadSchema,
@@ -15,6 +16,7 @@ import {
15
16
  steps
16
17
  } from '../models/index.js'
17
18
  import * as service from '../service.js'
19
+ import { resolveLanguage } from '../../../utils/utils.js'
18
20
 
19
21
  const viewName = 'postcode-lookup-details'
20
22
 
@@ -22,7 +24,7 @@ const viewName = 'postcode-lookup-details'
22
24
  * Get the session state associated with this journey
23
25
  * @param {PostcodeLookupRequest} request
24
26
  */
25
- function getSessionState(request) {
27
+ export function getSessionState(request) {
26
28
  /**
27
29
  * @type {PostcodeLookupSessionData | null | undefined}
28
30
  */
@@ -63,6 +65,32 @@ function flashComponentState(request, componentName, address) {
63
65
 
64
66
  request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true)
65
67
  }
68
+ /**
69
+ * Get a cached translator, or create a new one if not in the cache
70
+ * @param {PostcodeLookupRequest} request
71
+ * @param {PostcodeLookupDispatchData} initial
72
+ * @param { string | string[] | undefined } language
73
+ */
74
+ export async function getDispatchTranslator(request, initial, language) {
75
+ const { formId, formStatus } = initial
76
+
77
+ // Override language if passed as a query param
78
+ if (language) {
79
+ request.yar.set('language', language)
80
+ }
81
+
82
+ const typedReq = /** @type {AnyFormRequest} */ (/** @type {any} */ (request))
83
+ const lang = resolveLanguage(typedReq)
84
+
85
+ const translator = await getCachedPluginTranslator(
86
+ typedReq,
87
+ /** @type {FormMetadata} */ ({ id: formId }),
88
+ formStatus,
89
+ lang
90
+ )
91
+
92
+ return translator
93
+ }
66
94
 
67
95
  /**
68
96
  * Initialises and dispatches the request to the postcode lookup journey
@@ -101,15 +129,21 @@ function getRoute() {
101
129
  return {
102
130
  method: 'GET',
103
131
  path: JOURNEY_BASE_URL,
104
- handler(request, h) {
132
+ async handler(request, h) {
105
133
  const { query } = request
106
- const { step } = query
134
+ const { step, language } = query
107
135
  const session = getSessionState(request)
108
136
 
137
+ const translator = await getDispatchTranslator(
138
+ request,
139
+ session.initial,
140
+ language
141
+ )
142
+
109
143
  const model =
110
144
  step === steps.manual
111
- ? manualViewModel(session)
112
- : detailsViewModel(session)
145
+ ? manualViewModel(session, translator)
146
+ : detailsViewModel(session, translator)
113
147
 
114
148
  return h.view(viewName, model)
115
149
  },
@@ -117,7 +151,8 @@ function getRoute() {
117
151
  validate: {
118
152
  query: Joi.object()
119
153
  .keys({
120
- step: Joi.string().allow(steps.details, steps.manual).optional()
154
+ step: Joi.string().allow(steps.details, steps.manual).optional(),
155
+ language: Joi.string().allow('en-GB', 'cy').optional()
121
156
  })
122
157
  .optional()
123
158
  }
@@ -172,7 +207,13 @@ function postRoute(options) {
172
207
  async function detailsPostHandler(request, h, options) {
173
208
  const session = getSessionState(request)
174
209
  const { ordnanceSurveyApiKey: apiKey } = options
175
- const language = session.initial.language ?? 'en-GB'
210
+ const translator = await getDispatchTranslator(
211
+ request,
212
+ session.initial,
213
+ request.query.language
214
+ )
215
+ const language = translator.language
216
+
176
217
  const { value: details, error } = createDetailsPayloadSchema(
177
218
  language
178
219
  ).validate(request.payload)
@@ -180,7 +221,7 @@ async function detailsPostHandler(request, h, options) {
180
221
  let model
181
222
 
182
223
  if (error) {
183
- model = detailsViewModel(session, details, error)
224
+ model = detailsViewModel(session, translator, details, error)
184
225
 
185
226
  return h.view(viewName, model)
186
227
  }
@@ -191,7 +232,7 @@ async function detailsPostHandler(request, h, options) {
191
232
  // Store the updated session
192
233
  request.yar.set(JOURNEY_BASE_URL, session)
193
234
 
194
- model = await selectViewModel({ session, apiKey })
235
+ model = await selectViewModel({ session, apiKey }, translator)
195
236
 
196
237
  return h.view(viewName, model)
197
238
  }
@@ -205,13 +246,23 @@ async function detailsPostHandler(request, h, options) {
205
246
  async function selectPostHandler(request, h, options) {
206
247
  const session = getSessionState(request)
207
248
  const { ordnanceSurveyApiKey: apiKey } = options
208
- const language = session.initial.language ?? 'en-GB'
249
+ const translator = await getDispatchTranslator(
250
+ request,
251
+ session.initial,
252
+ request.query.language
253
+ )
254
+ const language = translator.language
209
255
  const { value: select, error } = createSelectPayloadSchema(language).validate(
210
256
  request.payload
211
257
  )
212
258
 
213
259
  if (error) {
214
- const model = await selectViewModel({ session, apiKey }, select, error)
260
+ const model = await selectViewModel(
261
+ { session, apiKey },
262
+ translator,
263
+ select,
264
+ error
265
+ )
215
266
 
216
267
  return h.view(viewName, model)
217
268
  }
@@ -235,9 +286,14 @@ async function selectPostHandler(request, h, options) {
235
286
  * @param {PostcodeLookupPostRequest} request
236
287
  * @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
237
288
  */
238
- function manualPostHandler(request, h) {
289
+ async function manualPostHandler(request, h) {
239
290
  const session = getSessionState(request)
240
- const language = session.initial.language ?? 'en-GB'
291
+ const translator = await getDispatchTranslator(
292
+ request,
293
+ session.initial,
294
+ request.query.language
295
+ )
296
+ const language = translator.language
241
297
 
242
298
  const { value: manual, error } = createManualPayloadSchema(language).validate(
243
299
  request.payload,
@@ -247,7 +303,7 @@ function manualPostHandler(request, h) {
247
303
  )
248
304
 
249
305
  if (error) {
250
- const model = manualViewModel(session, manual, error)
306
+ const model = manualViewModel(session, translator, manual, error)
251
307
 
252
308
  return h.view(viewName, model)
253
309
  }
@@ -261,7 +317,8 @@ function manualPostHandler(request, h) {
261
317
 
262
318
  /**
263
319
  * @import { ResponseToolkit, ServerRoute } from '@hapi/hapi'
264
- * @import { PostcodeLookupManualPayload, PostcodeLookupDetailsPayload, PostcodeLookupSelectPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '../types.js'
320
+ * @import { FormMetadata } from '@defra/forms-model'
321
+ * @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '../types.js'
265
322
  * @import { FormRequestPayload, FormResponseToolkit } from '../../../routes/types.js'
266
- * @import { ExternalStateAppendage } from '../../engine/types.js'
323
+ * @import { AnyFormRequest, ExternalStateAppendage } from '../../engine/types.js'
267
324
  */
@@ -14,12 +14,16 @@
14
14
  /**
15
15
  * @typedef {{
16
16
  * sourceUrl: string,
17
+ * formId: string
17
18
  * formName: string
19
+ * formStatus: FormStatus
20
+ * componentId: string
18
21
  * componentName: string
19
22
  * componentTitle: string,
20
23
  * componentHint?: string
21
24
  * step?: string
22
25
  * language?: string
26
+ * languages?: { name: string, code: string}[]
23
27
  * }} PostcodeLookupDispatchData
24
28
  */
25
29
 
@@ -49,6 +53,7 @@
49
53
  * Postcode lookup query params
50
54
  * @typedef {object} PostcodeLookupQuery
51
55
  * @property {string} [step] - step
56
+ * @property {string} [language] - language
52
57
  */
53
58
 
54
59
  /**
@@ -140,5 +145,5 @@
140
145
 
141
146
  /**
142
147
  * @import { Request } from '@hapi/hapi'
143
- * @import { FormPayload } from '../engine/types.js'
148
+ * @import { FormStatus } from '@defra/forms-model'
144
149
  */