diff --git a/src/server/plugins/engine/components/LocationFieldBase.ts b/src/server/plugins/engine/components/LocationFieldBase.ts index 34a9905a4..22480c3b5 100644 --- a/src/server/plugins/engine/components/LocationFieldBase.ts +++ b/src/server/plugins/engine/components/LocationFieldBase.ts @@ -125,9 +125,11 @@ export abstract class LocationFieldBase extends FormComponent { const viewModel = super.getViewModel(context) if (this.instructionText) { + const { tComponent } = context.translator return { ...viewModel, - instructionText: this.instructionText + instructionText: + tComponent(this.def, 'instructionText') || this.instructionText } } diff --git a/src/server/plugins/engine/components/LocationFieldHelpers.ts b/src/server/plugins/engine/components/LocationFieldHelpers.ts index 92b1d8de6..14431d9bc 100644 --- a/src/server/plugins/engine/components/LocationFieldHelpers.ts +++ b/src/server/plugins/engine/components/LocationFieldHelpers.ts @@ -198,9 +198,15 @@ export function getLocationFieldViewModel( } if (component.options.instructionText) { + const { t, tComponent } = context.translator + return { ...result, - instructionText: component.options.instructionText + instructionSummary: t('components.locationFieldBase.howToFind'), + instructionText: + tComponent(component.def, 'instructionText') || + component.options.instructionText, + t } } diff --git a/src/server/plugins/engine/form-availability.test.ts b/src/server/plugins/engine/form-availability.test.ts index 341c76275..10f52e2d5 100644 --- a/src/server/plugins/engine/form-availability.test.ts +++ b/src/server/plugins/engine/form-availability.test.ts @@ -13,6 +13,7 @@ describe('form-availability', () => { expect(() => assertFormAvailable( { ...metadata, offline: false }, + undefined, FormStatus.Live, false ) @@ -20,6 +21,7 @@ describe('form-availability', () => { expect(() => assertFormAvailable( { ...metadata, offline: undefined }, + undefined, FormStatus.Live, false ) @@ -30,6 +32,7 @@ describe('form-availability', () => { expect(() => assertFormAvailable( { ...metadata, offline: false }, + undefined, FormStatus.Draft, true ) @@ -37,6 +40,7 @@ describe('form-availability', () => { expect(() => assertFormAvailable( { ...metadata, offline: undefined }, + undefined, FormStatus.Live, true ) @@ -47,6 +51,7 @@ describe('form-availability', () => { expect(() => assertFormAvailable( { ...metadata, offline: true }, + undefined, FormStatus.Live, false ) diff --git a/src/server/plugins/engine/form-availability.ts b/src/server/plugins/engine/form-availability.ts index de140534e..d040fa6ea 100644 --- a/src/server/plugins/engine/form-availability.ts +++ b/src/server/plugins/engine/form-availability.ts @@ -1,9 +1,14 @@ -import { FormStatus, type FormMetadata } from '@defra/forms-model' +import { + FormStatus, + type FormDefinition, + type FormMetadata +} from '@defra/forms-model' import Boom from '@hapi/boom' export interface OfflineBoomData { offline: true metadata: FormMetadata + definition: FormDefinition | undefined } /** @@ -13,6 +18,7 @@ export interface OfflineBoomData { */ export function assertFormAvailable( metadata: FormMetadata, + definition: FormDefinition | undefined, formState: FormStatus, isPreview: boolean ): void { @@ -21,7 +27,7 @@ export function assertFormAvailable( formState === FormStatus.Live && !isPreview ) { - const data: OfflineBoomData = { offline: true, metadata } + const data: OfflineBoomData = { offline: true, metadata, definition } throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), { statusCode: 503, data diff --git a/src/server/plugins/engine/form-context.ts b/src/server/plugins/engine/form-context.ts index 304cf252d..5af70f920 100644 --- a/src/server/plugins/engine/form-context.ts +++ b/src/server/plugins/engine/form-context.ts @@ -56,13 +56,14 @@ export async function getFormModel( const formState = resolveState(state) const metadata = await formsService.getFormMetadata(slug) - assertFormAvailable(metadata, formState, isPreview) const definition = await formsService.getFormDefinition( metadata.id, formState ) + assertFormAvailable(metadata, definition, formState, isPreview) + if (!definition) { throw Boom.notFound( `No definition found for form metadata ${metadata.id} (${slug}) ${state}` @@ -145,7 +146,6 @@ export async function resolveFormModel( const metadata = await formsService.getFormMetadata(slug) const formState = resolveState(state) const isPreview = options.isPreview ?? isPreviewState(state, options) - assertFormAvailable(metadata, formState, isPreview) const stateMetadata = metadata[formState] if (!stateMetadata) { @@ -199,6 +199,8 @@ export async function resolveFormModel( cache.set(cacheKey, entry) } + assertFormAvailable(metadata, entry.model.def, formState, isPreview) + return entry.model } diff --git a/src/server/plugins/engine/i18n/translations/cy.json b/src/server/plugins/engine/i18n/translations/cy.json index d533368f6..7e3ed3077 100644 --- a/src/server/plugins/engine/i18n/translations/cy.json +++ b/src/server/plugins/engine/i18n/translations/cy.json @@ -207,6 +207,16 @@ "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." + }, + + "formUnavailable": { + "title": "Mae'n ddrwg gennyf, nid yw'r ffurflen hon ar gael", + "subHeading": "Nid yw '[[formTitle]]' ar gael mwyach.", + "contactDetailsHeading": "Manylion cyswllt ar gyfer [[organisationName]]", + "telephone": "Ffôn", + "callCharges": "Dysgwch am gostau galwadau", + "email": "E-bost", + "onlineContactForm": "Ffurflen gyswllt ar-lein" } }, diff --git a/src/server/plugins/engine/i18n/translations/en-GB.json b/src/server/plugins/engine/i18n/translations/en-GB.json index 2fa8b5ee3..370e1e06f 100644 --- a/src/server/plugins/engine/i18n/translations/en-GB.json +++ b/src/server/plugins/engine/i18n/translations/en-GB.json @@ -154,6 +154,16 @@ "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." + }, + + "formUnavailable": { + "title": "Sorry, this form is unavailable", + "subHeading": "'[[formTitle]]' is no longer available.", + "contactDetailsHeading": "Contact details for [[organisationName]]", + "telephone": "Telephone", + "callCharges": "Find out about call charges", + "email": "Email", + "onlineContactForm": "Online contact form" } }, diff --git a/src/server/plugins/engine/i18n/types.ts b/src/server/plugins/engine/i18n/types.ts index f9aaf3cfb..89efed627 100644 --- a/src/server/plugins/engine/i18n/types.ts +++ b/src/server/plugins/engine/i18n/types.ts @@ -24,6 +24,7 @@ export type FormDefinitionTranslations = Record< shortDescription: string errorDescription: string paymentDescription: string + instructionText: string }> > sections: Record>> diff --git a/src/server/plugins/engine/models/unavailable-view-model.test.ts b/src/server/plugins/engine/models/unavailable-view-model.test.ts index bc8f2ad80..2f7d9b909 100644 --- a/src/server/plugins/engine/models/unavailable-view-model.test.ts +++ b/src/server/plugins/engine/models/unavailable-view-model.test.ts @@ -1,24 +1,37 @@ -import { type FormMetadata } from '@defra/forms-model' - +import { CY, EN_GB } from '~/src/server/constants.js' import { unavailableViewModel } from '~/src/server/plugins/engine/models/unavailable-view-model.js' import { metadata } from '~/test/fixtures/form.js' describe('unavailableViewModel', () => { it('should return the correct view model with basic metadata', () => { - const result = unavailableViewModel(metadata) + const result = unavailableViewModel(metadata, undefined, EN_GB) expect(result).toEqual({ pageTitle: 'Sorry, this form is unavailable', formTitle: 'Test form', + language: 'en-GB', + languages: [ + { code: 'en-GB', name: 'English' }, + { code: 'cy', name: 'Cymraeg' } + ], organisationName: 'Defra', - phoneLines: undefined + contact: undefined, + context: expect.any(Object) }) }) - it('should strip the organisation suffix if present', () => { - const result = unavailableViewModel({ - ...metadata, - organisation: 'Rural Payments Agency - RPA' - } as FormMetadata) - expect(result.organisationName).toBe('Rural Payments Agency - RPA') + it('should handle Welsh', () => { + const result = unavailableViewModel(metadata, undefined, CY) + expect(result).toEqual({ + pageTitle: "Mae'n ddrwg gennyf, nid yw'r ffurflen hon ar gael", + formTitle: 'Test form', + language: 'cy', + languages: [ + { code: 'en-GB', name: 'English' }, + { code: 'cy', name: 'Cymraeg' } + ], + organisationName: 'Defra', + contact: undefined, + context: expect.any(Object) + }) }) }) diff --git a/src/server/plugins/engine/models/unavailable-view-model.ts b/src/server/plugins/engine/models/unavailable-view-model.ts index f69d477b9..b79643b48 100644 --- a/src/server/plugins/engine/models/unavailable-view-model.ts +++ b/src/server/plugins/engine/models/unavailable-view-model.ts @@ -1,19 +1,40 @@ -import { type FormMetadata, type FormMetadataContact } from '@defra/forms-model' +import { + type FormDefinition, + type FormMetadata, + type FormMetadataContact +} from '@defra/forms-model' + +import { createFormTranslator } from '~/src/server/plugins/engine/i18n/createFormTranslator.js' +import { getAvailableLanguages } from '~/src/server/plugins/engine/i18n/languages.js' +import { type Translator } from '~/src/server/plugins/engine/types/index.js' export interface UnavailableViewModel { pageTitle: string formTitle: string organisationName: string contact?: FormMetadataContact + context: { translator: Translator } + language: string + languages: { code: string; name: string }[] } export function unavailableViewModel( - metadata: FormMetadata + metadata: FormMetadata, + definition: FormDefinition | undefined, + language: string ): UnavailableViewModel { + const translator = createFormTranslator(metadata, definition, language) + const { t, tForm } = translator return { - pageTitle: 'Sorry, this form is unavailable', - formTitle: metadata.title, + pageTitle: t('pages.formUnavailable.title'), + formTitle: tForm('title') || metadata.title, organisationName: metadata.organisation, - contact: metadata.contact + contact: metadata.contact, + context: { translator }, + language, + // Always get Welsh and English + languages: getAvailableLanguages({ + metadata: { translations: { cy: {} } } + } as unknown as FormDefinition) } } diff --git a/src/server/plugins/engine/unavailable-response.test.ts b/src/server/plugins/engine/unavailable-response.test.ts index d5aca4118..7dc3e116a 100644 --- a/src/server/plugins/engine/unavailable-response.test.ts +++ b/src/server/plugins/engine/unavailable-response.test.ts @@ -6,7 +6,9 @@ import { type Server } from '@hapi/hapi' +import { EN_GB } from '~/src/server/constants.js' import * as availability from '~/src/server/plugins/engine/form-availability.js' +import { type Translator } from '~/src/server/plugins/engine/i18n/types.js' import * as viewModel from '~/src/server/plugins/engine/models/unavailable-view-model.js' import { registerUnavailableResponse } from '~/src/server/plugins/engine/unavailable-response.js' import { metadata } from '~/test/fixtures/form.js' @@ -56,7 +58,14 @@ describe('registerUnavailableResponse', () => { statusCode: 503, data: offlineData }) - const mockRequest = { response: mockResponse } as Request + const mockRequest = { + response: mockResponse, + server: { + plugins: { + 'forms-engine-plugin': {} + } + } + } as Request const mockViewResponse = { header: jest.fn().mockReturnThis(), @@ -75,18 +84,31 @@ describe('registerUnavailableResponse', () => { .mockReturnValue({ pageTitle: 'Unavailable', formTitle: 'Test', - organisationName: 'Defra' + organisationName: 'Defra', + language: EN_GB, + languages: [ + { code: 'en-GB', name: 'English' }, + { code: 'cy', name: 'Cymraeg' } + ], + context: { translator: {} as unknown as Translator } }) const result = await extensionHandler(mockRequest, mockH) expect(availability.isOfflineBoom).toHaveBeenCalledWith(mockResponse) - expect(viewModel.unavailableViewModel).toHaveBeenCalledWith(metadata) - expect(mockH.view).toHaveBeenCalledWith('unavailable', { - pageTitle: 'Unavailable', - formTitle: 'Test', - organisationName: 'Defra' - }) + expect(viewModel.unavailableViewModel).toHaveBeenCalledWith( + metadata, + undefined, + 'en-GB' + ) + expect(mockH.view).toHaveBeenCalledWith( + 'unavailable', + expect.objectContaining({ + pageTitle: 'Unavailable', + formTitle: 'Test', + organisationName: 'Defra' + }) + ) expect(mockViewResponse.header).toHaveBeenCalledWith( 'Cache-Control', 'no-store, no-cache, must-revalidate' diff --git a/src/server/plugins/engine/unavailable-response.ts b/src/server/plugins/engine/unavailable-response.ts index 768924b79..5e9fd57a3 100644 --- a/src/server/plugins/engine/unavailable-response.ts +++ b/src/server/plugins/engine/unavailable-response.ts @@ -2,6 +2,8 @@ import { type Request, type ResponseToolkit, type Server } from '@hapi/hapi' import { isOfflineBoom } from '~/src/server/plugins/engine/form-availability.js' import { unavailableViewModel } from '~/src/server/plugins/engine/models/unavailable-view-model.js' +import { type AnyFormRequest } from '~/src/server/plugins/engine/types.js' +import { resolveLanguage } from '~/src/server/utils/utils.js' /** * Registers a server-wide onPreResponse extension that intercepts the offline @@ -17,10 +19,12 @@ export function registerUnavailableResponse(server: Server) { return h.continue } - const { metadata } = response.data + const { metadata, definition } = response.data + + const language = resolveLanguage(request as unknown as AnyFormRequest) return h - .view('unavailable', unavailableViewModel(metadata)) + .view('unavailable', unavailableViewModel(metadata, definition, language)) .header('Cache-Control', 'no-store, no-cache, must-revalidate') .header('X-Robots-Tag', 'noindex, nofollow') .code(200) diff --git a/src/server/plugins/engine/views/components/_location-field-base.html b/src/server/plugins/engine/views/components/_location-field-base.html index 59c2f040d..cc600194e 100644 --- a/src/server/plugins/engine/views/components/_location-field-base.html +++ b/src/server/plugins/engine/views/components/_location-field-base.html @@ -61,7 +61,7 @@ {% set detailsHtml %} {% if component.model.instructionText %} {{ govukDetails({ - summaryText: t('components.locationFieldBase.howToFind'), + summaryText: component.model.instructionSummary, html: component.model.instructionText | markdown | safe, classes: "govuk-!-margin-top-3" }) }} diff --git a/src/server/plugins/engine/views/unavailable.html b/src/server/plugins/engine/views/unavailable.html index 6d61a5ff8..4ce14a5a6 100644 --- a/src/server/plugins/engine/views/unavailable.html +++ b/src/server/plugins/engine/views/unavailable.html @@ -1,32 +1,35 @@ {% extends baseLayoutPath %} +{% set t = context.translator.t %} +{% set tForm = context.translator.tForm %} + {% block content %}
-

Sorry, this form is unavailable

-

'{{ formTitle }}' is no longer available.

-

Contact details for {{ organisationName }}

+

{{ t('pages.formUnavailable.title') }}

+

{{ t('pages.formUnavailable.subHeading', { formTitle: formTitle }) }}

+

{{ t('pages.formUnavailable.contactDetailsHeading', { organisationName: organisationName }) }}

{% if contact.phone %} -

Telephone

+

{{ t('pages.formUnavailable.telephone') }}

- {{ contact.phone | markdown(3) | safe }} + {{ tForm('contact.phone') | markdown(3) | safe }}
-

Find out about call charges

+

{{ t('pages.formUnavailable.callCharges') }}

{% endif %} {% if contact.email %} -

Email

+

{{ t('pages.formUnavailable.email') }}

{% endif %} {% if contact.online %} -

Online contact form

+

{{ t('pages.formUnavailable.onlineContactForm') }}

{% endif %}