From d1bae8ea719cfe05caa1018e03b44a4ef6bfa2fa Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Fri, 17 Jul 2026 12:44:41 +0100 Subject: [PATCH 1/8] Fixed optional + welsh yes/no --- .../engine/i18n/createFormTranslator.ts | 19 ++++++++++++++++++- .../pageControllers/QuestionPageController.ts | 12 +++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/server/plugins/engine/i18n/createFormTranslator.ts b/src/server/plugins/engine/i18n/createFormTranslator.ts index 3c0b51e8a..9e3d8d065 100644 --- a/src/server/plugins/engine/i18n/createFormTranslator.ts +++ b/src/server/plugins/engine/i18n/createFormTranslator.ts @@ -1,4 +1,4 @@ -import { type FormDefinition } from '@defra/forms-model' +import { getYesNoList, type FormDefinition } from '@defra/forms-model' import { type i18n } from 'i18next' import { createTranslator } from '~/src/server/plugins/engine/i18n/createTranslator.js' @@ -28,6 +28,23 @@ export function loadFormTranslations(def: FormDefinition, i18nInstance: i18n) { if (formTranslations) { for (const [lng, resources] of Object.entries(formTranslations)) { i18nInstance.addResourceBundle(lng, 'form', resources, true, true) + + // Temporary workaround - until we develop a better solution + const yesNoList = getYesNoList() + if (lng === 'cy') { + i18nInstance.addResource( + lng, + 'form', + `listItems.${yesNoList.items[0].id}.text`, + 'Ie' + ) + i18nInstance.addResource( + lng, + 'form', + `listItems.${yesNoList.items[1].id}.text`, + 'Nage' + ) + } } } } diff --git a/src/server/plugins/engine/pageControllers/QuestionPageController.ts b/src/server/plugins/engine/pageControllers/QuestionPageController.ts index a7893e84f..037921e5b 100644 --- a/src/server/plugins/engine/pageControllers/QuestionPageController.ts +++ b/src/server/plugins/engine/pageControllers/QuestionPageController.ts @@ -174,9 +174,15 @@ export class QuestionPageController extends PageController { if (!pageTitle) { pageTitle = labelOrLegend.text } - labelOrLegend.text = isOptional - ? `${pageTitle} ${t('common.optional')}` - : pageTitle + + // The question text may have already appended 'optional' in the relevant language + const optionalAlreadyAppended = pageTitle.endsWith( + ` ${t('common.optional')}` + ) + labelOrLegend.text = + isOptional && !optionalAlreadyAppended + ? `${pageTitle} ${t('common.optional')}` + : pageTitle } // Fallback if still no pageTitle set From 47284837f819d6b82ed5dd01bc26d43b49d8e2ef Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 08:01:34 +0100 Subject: [PATCH 2/8] Fixes --- src/server/plugins/engine/components/PaymentField.ts | 7 ++++++- .../plugins/engine/i18n/createFormTranslator.ts | 11 +++++++---- src/server/plugins/engine/i18n/translations/cy.json | 2 +- .../plugins/engine/i18n/translations/en-GB.json | 2 +- src/server/plugins/engine/i18n/types.ts | 1 + .../engine/pageControllers/QuestionPageController.ts | 7 +++---- .../plugins/engine/views/components/paymentfield.html | 4 +++- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/server/plugins/engine/components/PaymentField.ts b/src/server/plugins/engine/components/PaymentField.ts index 53d63d028..556178568 100644 --- a/src/server/plugins/engine/components/PaymentField.ts +++ b/src/server/plugins/engine/components/PaymentField.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'node:crypto' import { + type ComponentDef, type FormMetadata, type PaymentFieldComponent } from '@defra/forms-model' @@ -110,6 +111,8 @@ export class PaymentField extends FormComponent { getViewModel(context: RenderContext) { const { payload } = context const viewModel = super.getViewModel(context) + const { translator } = context + const { tComponent } = translator // Payload is pre-populated from state if a payment has already been made const paymentState = this.isPaymentState(payload[this.name] as unknown) @@ -124,7 +127,9 @@ export class PaymentField extends FormComponent { return { ...viewModel, amount: formatCurrency(amount), - description: this.options.description, + description: + tComponent(this as unknown as ComponentDef, 'paymentDescription') || + this.options.description, paymentState } } diff --git a/src/server/plugins/engine/i18n/createFormTranslator.ts b/src/server/plugins/engine/i18n/createFormTranslator.ts index 9e3d8d065..ddeae11c0 100644 --- a/src/server/plugins/engine/i18n/createFormTranslator.ts +++ b/src/server/plugins/engine/i18n/createFormTranslator.ts @@ -1,4 +1,8 @@ -import { getYesNoList, type FormDefinition } from '@defra/forms-model' +import { + yesNoListNoItemId, + yesNoListYesItemId, + type FormDefinition +} from '@defra/forms-model' import { type i18n } from 'i18next' import { createTranslator } from '~/src/server/plugins/engine/i18n/createTranslator.js' @@ -30,18 +34,17 @@ export function loadFormTranslations(def: FormDefinition, i18nInstance: i18n) { i18nInstance.addResourceBundle(lng, 'form', resources, true, true) // Temporary workaround - until we develop a better solution - const yesNoList = getYesNoList() if (lng === 'cy') { i18nInstance.addResource( lng, 'form', - `listItems.${yesNoList.items[0].id}.text`, + `listItems.${yesNoListYesItemId}.text`, 'Ie' ) i18nInstance.addResource( lng, 'form', - `listItems.${yesNoList.items[1].id}.text`, + `listItems.${yesNoListNoItemId}.text`, 'Nage' ) } diff --git a/src/server/plugins/engine/i18n/translations/cy.json b/src/server/plugins/engine/i18n/translations/cy.json index 25d86a998..388115535 100644 --- a/src/server/plugins/engine/i18n/translations/cy.json +++ b/src/server/plugins/engine/i18n/translations/cy.json @@ -245,7 +245,7 @@ }, "paymentField": { - "detailsRequired": "Angen manylion talu", + "detailsRequired": "Angen talu", "pendingTransaction": "Mae'n bosibl y gwelwch drafodiad ar y gweill yn eich cyfrif banc ond dim ond pan fyddwch yn cyflwyno'r ffurflen y codir tâl arnoch.", "addPaymentDetails": "Ychwanegu manylion talu", "cannotTakePayment": "Mae problem ac ni allwn dderbyn taliad. Cysylltwch â ni (manylion yn nhroedyn y ffurflen hon) neu cadwch eich cynnydd a dychwelyd at y ffurflen yn ddiweddarach.", diff --git a/src/server/plugins/engine/i18n/translations/en-GB.json b/src/server/plugins/engine/i18n/translations/en-GB.json index 507ef8027..155b4c169 100644 --- a/src/server/plugins/engine/i18n/translations/en-GB.json +++ b/src/server/plugins/engine/i18n/translations/en-GB.json @@ -245,7 +245,7 @@ }, "paymentField": { - "detailsRequired": "Payment details required", + "detailsRequired": "Payment required", "pendingTransaction": "You may see a pending transaction in your bank account but you will only be charged when you submit the form.", "addPaymentDetails": "Add payment details", "cannotTakePayment": "There is a problem and we cannot take a payment. Contact us (details in the footer of this form) or save your progress and return to the form later.", diff --git a/src/server/plugins/engine/i18n/types.ts b/src/server/plugins/engine/i18n/types.ts index 8aa665f56..a10db632f 100644 --- a/src/server/plugins/engine/i18n/types.ts +++ b/src/server/plugins/engine/i18n/types.ts @@ -22,6 +22,7 @@ export type FormDefinitionTranslations = Record< hint: string content: string shortDescription: string + paymentDescription: string }> > sections: Record>> diff --git a/src/server/plugins/engine/pageControllers/QuestionPageController.ts b/src/server/plugins/engine/pageControllers/QuestionPageController.ts index 037921e5b..3c6f6281e 100644 --- a/src/server/plugins/engine/pageControllers/QuestionPageController.ts +++ b/src/server/plugins/engine/pageControllers/QuestionPageController.ts @@ -176,12 +176,11 @@ export class QuestionPageController extends PageController { } // The question text may have already appended 'optional' in the relevant language - const optionalAlreadyAppended = pageTitle.endsWith( - ` ${t('common.optional')}` - ) + const optionalWord = t('common.optional') + const optionalAlreadyAppended = pageTitle.endsWith(` ${optionalWord}`) labelOrLegend.text = isOptional && !optionalAlreadyAppended - ? `${pageTitle} ${t('common.optional')}` + ? `${pageTitle} ${optionalWord}` : pageTitle } diff --git a/src/server/plugins/engine/views/components/paymentfield.html b/src/server/plugins/engine/views/components/paymentfield.html index 7f0bf8c8f..d72f45983 100644 --- a/src/server/plugins/engine/views/components/paymentfield.html +++ b/src/server/plugins/engine/views/components/paymentfield.html @@ -10,13 +10,15 @@
{% if isPreAuthorised %} +

model pre-auth:{{ model | dump }}

{# Payment already pre-authorised - show confirmation message #}

{{ t('components.paymentField.preAuthorisedTitle') }}

{{ t('components.paymentField.preAuthorisedBody') }}

{% else %} +

model:{{ model | dump }}

{# No pre-authorisation - show payment form #} -

{{ model.label.text if model.label and model.label.text else t('components.paymentField.detailsRequired') }}

+

{{ t('components.paymentField.detailsRequired') }}

{{ description }}

From 7fc3a652ae1f1c1882aa9173571bac3c0eb1f97a Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 08:04:46 +0100 Subject: [PATCH 3/8] Corrected 'No' in cy.json --- src/server/plugins/engine/i18n/translations/cy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/plugins/engine/i18n/translations/cy.json b/src/server/plugins/engine/i18n/translations/cy.json index 388115535..a3b7680b1 100644 --- a/src/server/plugins/engine/i18n/translations/cy.json +++ b/src/server/plugins/engine/i18n/translations/cy.json @@ -220,7 +220,7 @@ "yesNoField": { "yes": "Ie", - "no": "Na", + "no": "Nage", "validation": { "selectYesNoRequired": "{{#label}} - dewiswch ie neu na" } From 21d57735b30d2184590a437a6e6fbd58e26f5584 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 08:49:40 +0100 Subject: [PATCH 4/8] Removed debug --- src/server/plugins/engine/views/components/paymentfield.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/plugins/engine/views/components/paymentfield.html b/src/server/plugins/engine/views/components/paymentfield.html index d72f45983..63c760098 100644 --- a/src/server/plugins/engine/views/components/paymentfield.html +++ b/src/server/plugins/engine/views/components/paymentfield.html @@ -10,13 +10,11 @@
{% if isPreAuthorised %} -

model pre-auth:{{ model | dump }}

{# Payment already pre-authorised - show confirmation message #}

{{ t('components.paymentField.preAuthorisedTitle') }}

{{ t('components.paymentField.preAuthorisedBody') }}

{% else %} -

model:{{ model | dump }}

{# No pre-authorisation - show payment form #}

{{ t('components.paymentField.detailsRequired') }}

From cbd47d2f9f0c8fdc23dfecf89963d38ecb648e68 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 09:14:31 +0100 Subject: [PATCH 5/8] Translates 'Important' in preview banner --- src/server/plugins/engine/i18n/translations/cy.json | 1 + src/server/plugins/engine/i18n/translations/en-GB.json | 1 + src/server/plugins/engine/views/partials/preview-banner.html | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/plugins/engine/i18n/translations/cy.json b/src/server/plugins/engine/i18n/translations/cy.json index a3b7680b1..d1238d20b 100644 --- a/src/server/plugins/engine/i18n/translations/cy.json +++ b/src/server/plugins/engine/i18n/translations/cy.json @@ -111,6 +111,7 @@ "body": "Os hoffech dderbyn e-bost prawf, [[link]] yr hoffech i gyflwyniadau ffurflen fynd iddo.", "linkText": "nodwch y cyfeiriad e-bost (yn agor mewn tab newydd)" }, + "bannerTitle": "Pwysig", "bannerForce": "Rhagolwg o ffurflen [[previewMode]] yw hon. Peidiwch â rhoi gwybodaeth bersonol.", "bannerText": "Dyma ragolwg o dudalen ffurflen [[previewMode]] rydych chi'n ei golygu.", "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." diff --git a/src/server/plugins/engine/i18n/translations/en-GB.json b/src/server/plugins/engine/i18n/translations/en-GB.json index 155b4c169..b802221e5 100644 --- a/src/server/plugins/engine/i18n/translations/en-GB.json +++ b/src/server/plugins/engine/i18n/translations/en-GB.json @@ -111,6 +111,7 @@ "body": "If you want to receive a test email, [[link]] you want form submissions to go to.", "linkText": "enter the email address (opens in new tab)" }, + "bannerTitle": "Important", "bannerForce": "This is a preview of a [[previewMode]] form. Do not enter personal information.", "bannerText": "This is a preview of a [[previewMode]] form page you are editing.", "previousPages": "It depends on answers from earlier pages in the form. In the live version, users will need to complete those questions first." diff --git a/src/server/plugins/engine/views/partials/preview-banner.html b/src/server/plugins/engine/views/partials/preview-banner.html index 3d4cc427b..4c151c3f3 100644 --- a/src/server/plugins/engine/views/partials/preview-banner.html +++ b/src/server/plugins/engine/views/partials/preview-banner.html @@ -8,7 +8,9 @@

{%- endmacro -%} -{% call govukNotificationBanner() %} +{% call govukNotificationBanner({ + titleText: t('pages.preview.bannerTitle') +}) %} {% set previewModeTranslated = t('common.' + previewMode) %} {% if not context.isForceAccess %}

From 8827a1e8cefc004fc095d161d69119a704b09759 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 11:06:30 +0100 Subject: [PATCH 6/8] Handles translations for characterCount --- src/server/plugins/engine/i18n/translations/cy.json | 12 ++++++++++++ .../plugins/engine/i18n/translations/en-GB.json | 12 ++++++++++++ .../engine/views/components/multilinetextfield.html | 13 ++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/server/plugins/engine/i18n/translations/cy.json b/src/server/plugins/engine/i18n/translations/cy.json index d1238d20b..cc1a29f9f 100644 --- a/src/server/plugins/engine/i18n/translations/cy.json +++ b/src/server/plugins/engine/i18n/translations/cy.json @@ -267,6 +267,18 @@ "uploadedFilesHeading": "Ffeiliau a uwchlwythwyd", "filesCount_one": "Uwchlwythwyd [[count]] ffeil", "filesCount_other": "Uwchlwythwyd [[count]] ffeil" + }, + + "characterCount": { + "underLimit": { + "one": "Mae gennych %{count} nod ar ôl", + "other": "Mae gennych %{count} nod ar ôl" + }, + "atLimit": "Mae gennych 0 nod ar ôl", + "overLimit": { + "one": "Mae gennych %{count} nod yn ormod", + "other": "Mae gennych %{count} nod yn ormod" + } } } } diff --git a/src/server/plugins/engine/i18n/translations/en-GB.json b/src/server/plugins/engine/i18n/translations/en-GB.json index b802221e5..29cb2e1f1 100644 --- a/src/server/plugins/engine/i18n/translations/en-GB.json +++ b/src/server/plugins/engine/i18n/translations/en-GB.json @@ -267,6 +267,18 @@ "uploadedFilesHeading": "Uploaded files", "filesCount_one": "[[count]] file uploaded", "filesCount_other": "[[count]] files uploaded" + }, + + "characterCount": { + "underLimit": { + "one": "You have %{count} character remaining", + "other": "You have %{count} characters remaining" + }, + "atLimit": "You have 0 characters remaining", + "overLimit": { + "one": "You have %{count} character too many", + "other": "You have %{count} characters too many" + } } } } diff --git a/src/server/plugins/engine/views/components/multilinetextfield.html b/src/server/plugins/engine/views/components/multilinetextfield.html index fa3eb9480..6460a63cc 100644 --- a/src/server/plugins/engine/views/components/multilinetextfield.html +++ b/src/server/plugins/engine/views/components/multilinetextfield.html @@ -3,7 +3,18 @@ {% macro MultilineTextField(component) %} {% if component.model.isCharacterOrWordCount == true %} - {{ govukCharacterCount(component.model) }} + {% set characterParams = component.model | merge({ + charactersUnderLimitText: { + one: t('components.characterCount.underLimit.one'), + other: t('components.characterCount.underLimit.other') + }, + charactersAtLimitText: t('components.characterCount.atLimit'), + charactersOverLimitText: { + one: t('components.characterCount.overLimit.one'), + other: t('components.characterCount.overLimit.other') + } + } ) %} + {{ govukCharacterCount(characterParams) }} {% else %} {{ govukTextarea(component.model) }} {% endif %} From 20f35677ba689de3991712706d7bbfe56b9ac341 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 11:39:54 +0100 Subject: [PATCH 7/8] Removed 'as unknown' for PaymentFIeld --- src/server/plugins/engine/components/PaymentField.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/plugins/engine/components/PaymentField.ts b/src/server/plugins/engine/components/PaymentField.ts index 556178568..5fd2425dc 100644 --- a/src/server/plugins/engine/components/PaymentField.ts +++ b/src/server/plugins/engine/components/PaymentField.ts @@ -1,7 +1,6 @@ import { randomUUID } from 'node:crypto' import { - type ComponentDef, type FormMetadata, type PaymentFieldComponent } from '@defra/forms-model' @@ -46,6 +45,7 @@ export class PaymentField extends FormComponent { declare options: PaymentFieldComponent['options'] declare formSchema: ObjectSchema declare stateSchema: ObjectSchema + declare def: PaymentFieldComponent isAppendageStateSingleObject = true constructor( @@ -55,6 +55,7 @@ export class PaymentField extends FormComponent { super(def, props) this.options = def.options + this.def = def const paymentStateSchema = joi .object({ @@ -128,8 +129,7 @@ export class PaymentField extends FormComponent { ...viewModel, amount: formatCurrency(amount), description: - tComponent(this as unknown as ComponentDef, 'paymentDescription') || - this.options.description, + tComponent(this.def, 'paymentDescription') || this.options.description, paymentState } } From 65755ce028c1eebdbba0bbfd1cd2eccce99a4cec Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 20 Jul 2026 11:49:40 +0100 Subject: [PATCH 8/8] Added comment in JSON --- src/server/plugins/engine/i18n/translations/cy.json | 1 + src/server/plugins/engine/i18n/translations/en-GB.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/server/plugins/engine/i18n/translations/cy.json b/src/server/plugins/engine/i18n/translations/cy.json index cc1a29f9f..9697c726e 100644 --- a/src/server/plugins/engine/i18n/translations/cy.json +++ b/src/server/plugins/engine/i18n/translations/cy.json @@ -270,6 +270,7 @@ }, "characterCount": { + "_comment": "Note the use of %{count} instead of [[count]]. This is because we are passing to the govuk template, not passing to Joi", "underLimit": { "one": "Mae gennych %{count} nod ar ôl", "other": "Mae gennych %{count} nod ar ôl" diff --git a/src/server/plugins/engine/i18n/translations/en-GB.json b/src/server/plugins/engine/i18n/translations/en-GB.json index 29cb2e1f1..7eb2e3852 100644 --- a/src/server/plugins/engine/i18n/translations/en-GB.json +++ b/src/server/plugins/engine/i18n/translations/en-GB.json @@ -270,6 +270,7 @@ }, "characterCount": { + "_comment": "Note the use of %{count} instead of [[count]]. This is because we are passing to the govuk template, not passing to Joi", "underLimit": { "one": "You have %{count} character remaining", "other": "You have %{count} characters remaining"