diff --git a/docs/features/code-based/page-controllers.md b/docs/features/code-based/page-controllers.md index 9db306257..ac29f9b6a 100644 --- a/docs/features/code-based/page-controllers.md +++ b/docs/features/code-based/page-controllers.md @@ -63,11 +63,18 @@ Both are imported from `@defra/forms-engine-plugin/controllers/.js`. ## Examples -- [Changing the Nunjucks template](#changing-the-nunjucks-template) -- [Fetching data for the view model](#fetching-data-for-the-view-model) -- [Intercepting the GET handler](#intercepting-the-get-handler) -- [Writing to state on POST](#writing-to-state-on-post) -- [Display-only page (no form components)](#display-only-page-no-form-components) +- [Custom page controllers](#custom-page-controllers) + - [How it works](#how-it-works) + - [Choosing a base class](#choosing-a-base-class) + - [Examples](#examples) + - [Changing the Nunjucks template](#changing-the-nunjucks-template) + - [Fetching data for the view model](#fetching-data-for-the-view-model) + - [Intercepting the GET handler](#intercepting-the-get-handler) + - [Writing to state on POST](#writing-to-state-on-post) + - [Display-only page (no form components)](#display-only-page-no-form-components) + - [Reference](#reference) + - [What QuestionPageController gives you](#what-questionpagecontroller-gives-you) + - [Overridable members](#overridable-members) ### Changing the Nunjucks template @@ -99,11 +106,14 @@ class SelectGrantSchemeController extends QuestionPageController { return async (request: FormRequest, context: FormContext, h: FormResponseToolkit) => { const farmType = context.state.farmType + // Support for multiple languages + const translator = this.getTranslator(request) + // Fetch grant schemes available for the user's farm type const grantSchemes = await getEligibleGrantSchemes(farmType) // Build the standard view model and add the fetched data - const viewModel = this.getViewModel(request, context) + const viewModel = this.getViewModel(request, context, translator) return h.view(this.viewName, { ...viewModel, grantSchemes }) } @@ -153,9 +163,12 @@ import type { FormContext, FormRequestPayload, FormResponseToolkit } from '@defr class PassportLookupController extends QuestionPageController { makePostRouteHandler() { return async (request: FormRequestPayload, context: FormContext, h: FormResponseToolkit) => { + // Support for multiple languages + const translator = this.getTranslator(request) + // Re-render with component validation errors if any if (context.errors) { - const viewModel = this.getViewModel(request, context) + const viewModel = this.getViewModel(request, context, translator) return h.view(this.viewName, viewModel) } @@ -167,7 +180,7 @@ class PassportLookupController extends QuestionPageController { if (!passport) { // Re-render with a custom error — the input passed component validation // (format/required checks) but was not found in the external system - const viewModel = this.getViewModel(request, context) + const viewModel = this.getViewModel(request, context, translator) viewModel.errors = [{ text: 'Passport number not recognised. Check and try again.' }] return h.view(this.viewName, viewModel) } @@ -222,10 +235,10 @@ class IneligiblePageController extends PageController { ### Overridable members -| Member | Description | -| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `viewName` | The Nunjucks template rendered for this page. Defaults to `'index'`. Set `view` on the page definition to override. | -| `allowSaveAndExit` | Whether the "Save and exit" button is shown. `true` on `QuestionPageController`, `false` on `PageController`. Override as a class property to change the default. | -| `getViewModel(request, context)` | Returns the view model passed to the Nunjucks template. Override to add or modify properties synchronously. Only available on `QuestionPageController`. | -| `makeGetRouteHandler()` | Returns the async GET handler function. Override to control page load behaviour, including async data fetching. | -| `makePostRouteHandler()` | Returns the async POST handler function. Override to control form submission behaviour and write custom data to state. | +| Member | Description | +| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `viewName` | The Nunjucks template rendered for this page. Defaults to `'index'`. Set `view` on the page definition to override. | +| `allowSaveAndExit` | Whether the "Save and exit" button is shown. `true` on `QuestionPageController`, `false` on `PageController`. Override as a class property to change the default. | +| `getViewModel(request, context, translator)` | Returns the view model passed to the Nunjucks template. Override to add or modify properties synchronously. Only available on `QuestionPageController`. | +| `makeGetRouteHandler()` | Returns the async GET handler function. Override to control page load behaviour, including async data fetching. | +| `makePostRouteHandler()` | Returns the async POST handler function. Override to control form submission behaviour and write custom data to state. | diff --git a/docs/features/index.md b/docs/features/index.md index 4220be070..f87bcd730 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -31,3 +31,7 @@ Drive advanced functionality — such as calling APIs and rendering dynamic cont ### [Code-based Features](./features/code-based) Implement highly tailored behaviour by writing custom TypeScript/JavaScript that integrates with forms-engine-plugin's extension points. + +## [Multi-language support](./multi-language.md) + +Multiple languages, specifically English and Welsh, can be configured. diff --git a/docs/features/multi-language.md b/docs/features/multi-language.md new file mode 100644 index 000000000..fd149965e --- /dev/null +++ b/docs/features/multi-language.md @@ -0,0 +1,25 @@ +# Multi-language (specifically English and Welsh) + +The `engine-plugin` supports multi-languages (if configured). +Additional languages are configured by adding translations json files and adding a `translations` property under the `metadata` property of the form definition. +You can use the Designer to edit Welsh translations for your form (to insert the necessary values in the `metadata.translations` property). + +Currently only English and Welsh are configured but other languages could be added if desired. + +## Forms engine plugin + +The `engine-plugin` handles translations at two levels: + +1. Boilerplate text - a translation file per language exists in `plugin` (`en-GB.json` and `cy.json`). These provide English and Welsh for each boilerplate text element, such as the feedback link url and text, or the error summary etc. +2. Dynamic form text - English translations are read from the form definition (such as component title, short description etc), whereas Welsh translations are read from the `metadata.translations.cy` property of the form definition. + +The translator is constructed in the constructor of the `FormModel`. + +The translator provides a series of functions: + +- t(key) - a general translation function that takes a key (for example 'validation.numberMax') and returns either the appropriate English text or Welsh text deending on which is the current language +- tPage(key) - returns the appropriate translations for page elements, such as title, guidance etc. +- tComponent(key) - returns the appropriate translations for components, such as question text, hint, short description etc. +- tSection(key) - returns the appropriate translations for sections +- tListItem(key) - returns the appropriate translations for list items since each list value (e.g from a radio/checkbox etc) must have a translated equivalent +- tForm(key) - returns the appropriate translations for form-level elements. Currently this only handles the form name. diff --git a/jest.setup.cjs b/jest.setup.cjs index 2686c4c7f..8ac092a83 100644 --- a/jest.setup.cjs +++ b/jest.setup.cjs @@ -1,3 +1,10 @@ +// Pre-load @defra/forms-model to ensure Joi internals are initialised before any test +// file runs. This prevents a Symbol identity mismatch between the ESM-wrapped Joi +// instance (created by babel-jest) and the CJS Joi instance used by Joi's internal +// Template.isTemplate() check, which occurs when resetModules: true clears the +// registry between tests. +require('@defra/forms-model') + process.env.REDIS_HOST = 'dummy' process.env.REDIS_KEY_PREFIX = 'forms-designer' process.env.REDIS_PASSWORD = 'dummy' diff --git a/package-lock.json b/package-lock.json index 83064fc8a..6177a4dc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "humanize-duration": "^3.33.1", + "i18next": "^26.0.5", "ioredis": "^5.8.2", "joi": "^17.13.3", "liquidjs": "^10.24.0", @@ -150,7 +151,7 @@ }, "engines": { "node": ">=22.11.0 <25.0.0", - "npm": ">=10.9.0 <11.6.4" + "npm": ">=10.9.0 <=11.17.0" } }, "node_modules/@adobe/css-tools": { @@ -1981,9 +1982,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -20196,6 +20197,37 @@ "node": ">=10.18" } }, + "node_modules/i18next": { + "version": "26.0.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.5.tgz", + "integrity": "sha512-9uHb4T27TdV36phJXcbpnRPt5yzAfqHXVrdASvmHZyPuZJtrLythd+GyXhiaHV5LlpuuskbAqhwPjmfTbKbi8w==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -23172,9 +23204,9 @@ } }, "node_modules/liquidjs": { - "version": "10.27.0", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.27.0.tgz", - "integrity": "sha512-tw/OA59K7aIBlMKIrKlumr37fiZUheShVHXY8cVctWisgY1p9mc5hreOvlreoS0wTiwlWk14Ya7305c2a/Cg5w==", + "version": "10.27.1", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.27.1.tgz", + "integrity": "sha512-ylE+1q2kSef1UxAyxqbyuWM3FRWS1v48JK1Y3CoW3bD6TSNXZh0+GsVnihujEpKyR+Jejx2aRAFfC3AHm9rElg==", "license": "MIT", "dependencies": { "commander": "^10.0.0" diff --git a/package.json b/package.json index 68ef845be..ad861377e 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ }, "engines": { "node": ">=22.11.0 <25.0.0", - "npm": ">=10.9.0 <11.6.4" + "npm": ">=10.9.0 <=11.17.0" }, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -126,6 +126,7 @@ "highlight.js": "^11.11.1", "http-status-codes": "^2.3.0", "humanize-duration": "^3.33.1", + "i18next": "^26.0.5", "ioredis": "^5.8.2", "joi": "^17.13.3", "liquidjs": "^10.24.0", diff --git a/scripts/generate-component-previews.js b/scripts/generate-component-previews.js index 9ab7856b3..59c492d8d 100644 --- a/scripts/generate-component-previews.js +++ b/scripts/generate-component-previews.js @@ -5,6 +5,7 @@ import { markdownToHtml } from '@defra/forms-model' // Static imports so Jest can mock them (dynamic computed-path imports cannot be mocked). import { createComponent } from '~/src/server/plugins/engine/components/helpers/components.js' +import { stubTranslator } from '~/src/server/plugins/engine/pageControllers/__stubs__/translator.js' import { environment } from '~/src/server/plugins/nunjucks/environment.js' // Register the markdown filter that the engine plugin normally adds at server init. @@ -26,7 +27,11 @@ export function renderComponent(fixture) { /** @type {unknown} */ (fixture.model) ) const component = createComponent(fixture.def, { model }) - const viewModel = component.getViewModel(fixture.payload, []) + const viewModel = component.getViewModel({ + payload: fixture.payload, + errors: [], + translator: stubTranslator + }) // Apply large label/legend sizing to match how QuestionPageController styles // a single-component page. isPageHeading is intentionally omitted — setting it diff --git a/scripts/generate-component-previews.test.js b/scripts/generate-component-previews.test.js index 033e6213e..dc91aa861 100644 --- a/scripts/generate-component-previews.test.js +++ b/scripts/generate-component-previews.test.js @@ -27,6 +27,7 @@ import { } from './generate-component-previews.js' import { createComponent } from '~/src/server/plugins/engine/components/helpers/components.ts' +import { stubTranslator } from '~/src/server/plugins/engine/pageControllers/__stubs__/translator.js' import { environment } from '~/src/server/plugins/nunjucks/environment.js' describe('component-preview-fixtures', () => { @@ -132,6 +133,10 @@ describe('buildPartialMdx', () => { describe('renderComponent', () => { let mockGetViewModel + const mockModel = { + createTranslator: () => stubTranslator + } + beforeEach(() => { mockGetViewModel = jest.fn().mockReturnValue({ type: 'TextField', @@ -145,22 +150,32 @@ describe('renderComponent', () => { }) it('calls createComponent with def and model from fixture', () => { - renderComponent(fixtures.TextField) + renderComponent({ + ...fixtures.TextField, + model: mockModel + }) expect(createComponent).toHaveBeenCalledWith(fixtures.TextField.def, { - model: fixtures.TextField.model + model: mockModel }) }) it('calls getViewModel with payload and empty errors array', () => { - renderComponent(fixtures.TextField) - expect(mockGetViewModel).toHaveBeenCalledWith( - fixtures.TextField.payload, - [] - ) + renderComponent({ + ...fixtures.TextField, + model: mockModel + }) + expect(mockGetViewModel).toHaveBeenCalledWith({ + payload: {}, + errors: [], + translator: stubTranslator + }) }) it('passes viewModel wrapped as { type, model } to renderString', () => { - renderComponent(fixtures.TextField) + renderComponent({ + ...fixtures.TextField, + model: mockModel + }) expect(environment.renderString).toHaveBeenCalledWith( expect.stringContaining('componentList'), expect.objectContaining({ diff --git a/scripts/page-preview-fixtures.js b/scripts/page-preview-fixtures.js index bc4858d29..a4452f180 100644 --- a/scripts/page-preview-fixtures.js +++ b/scripts/page-preview-fixtures.js @@ -65,9 +65,14 @@ function pageViewContext({ server: { plugins: { 'forms-engine-plugin': {} } } }) ) + const translator = model.createTranslator() return getViewModelOverride ? getViewModelOverride(controller, model, mockRequest, mockContext) - : controller.getViewModel(/** @type {any} */ (mockRequest), mockContext) + : controller.getViewModel( + /** @type {any} */ (mockRequest), + mockContext, + translator + ) } const fileUploadWithFilesVariant = /** @type {any} */ ( @@ -192,13 +197,18 @@ export const pageFixtures = { ] } ], - getViewModelOverride: (ctrl, _model, req, ctx) => { + getViewModelOverride: (ctrl, model, req, ctx) => { const repeat = /** @type {RepeatPageController} */ (ctrl) - const vm = repeat.getListSummaryViewModel(req, ctx, [ - { itemId: '1', fullname: 'Sarah Phillips' }, - { itemId: '2', fullname: 'David Jones' }, - { itemId: '3', fullname: 'Emma Wilson' } - ]) + const vm = repeat.getListSummaryViewModel( + req, + ctx, + [ + { itemId: '1', fullname: 'Sarah Phillips' }, + { itemId: '2', fullname: 'David Jones' }, + { itemId: '3', fullname: 'Emma Wilson' } + ], + model.createTranslator() + ) return /** @type {PageViewModel} */ ( /** @type {unknown} */ ({ ...vm, @@ -300,10 +310,12 @@ export const pageFixtures = { ], renderPage: '/summary', state: { fullname: 'Sarah Phillips', email: 'sarah@example.gov.uk' }, - getViewModelOverride: (ctrl, _model, req, ctx) => { + getViewModelOverride: (ctrl, model, req, ctx) => { const summary = /** @type {SummaryPageController} */ (ctrl) return /** @type {PageViewModel} */ ( - /** @type {unknown} */ (summary.getSummaryViewModel(req, ctx)) + /** @type {unknown} */ ( + summary.getSummaryViewModel(req, ctx, model.createTranslator()) + ) ) } }) diff --git a/src/server/forms/register-as-a-unicorn-breeder.yaml b/src/server/forms/register-as-a-unicorn-breeder.yaml index 66683758b..fa653641d 100644 --- a/src/server/forms/register-as-a-unicorn-breeder.yaml +++ b/src/server/forms/register-as-a-unicorn-breeder.yaml @@ -242,20 +242,6 @@ pages: content: 'Fill in this field' options: required: false - next: - - path: '/pay-for-your-licence' - - title: Pay for your licence - path: '/pay-for-your-licence' - section: section - components: - - name: licencePayment - title: Unicorn breeder licence fee - type: PaymentField - hint: You'll be redirected to GOV.UK Pay to complete your payment - options: - required: true - amount: 50 - description: Unicorn breeder annual licence fee next: - path: '/summary' conditions: diff --git a/src/server/forms/simple-form.yaml b/src/server/forms/simple-form.yaml index af1f92f38..06cc23655 100644 --- a/src/server/forms/simple-form.yaml +++ b/src/server/forms/simple-form.yaml @@ -3,6 +3,57 @@ name: Page events engine: V2 schema: 2 startPage: '/summary' +metadata: + language: en-GB + translations: + cy: + components: + 1fb8e182-c709-4792-8f83-e01d8b1fee1a: + title: Beth yw eich enw cyntaf? + shortDescription: Eich enw cyntaf + b68df7f1-d4f4-4c17-83c8-402f584906c9: + title: Beth yw eich enw olaf? + shortDescription: Eich enw olaf + 987c1234-56d7-89e0-1234-56789abcdef0: + title: Llwytho copi o'ch pasbort + shortDescription: Llwytho pasbort + 987c1234-56d7-89e0-1234-56789abcdef1: + title: Llwytho copi o'ch trwydded yrru + shortDescription: Llwytho trwydded yrru + 00738799-3489-4ab2-a57b-542eecb31bfa: + title: Pryd yw pen-blwydd {{ applicantFirstName }} {{ applicantLastName }}? + shortDescription: Eich pen-blwydd + 123e4567-e89b-12d3-a456-426614174000: + title: Beth yw eich cyfeiriad? + shortDescription: Eich cyfeiriad + aabbccdd-1122-3344-5566-778899001123: + title: Oes gennych chi drwydded yrru? + shortDescription: Trwydded yrru + aabbccdd-1122-3344-5566-778899001124: + title: Sut rydych chi fel arfer yn teithio? + shortDescription: Eich ffordd o deithio + listItems: + aabbccdd-1122-3344-5566-778899001126: + text: Car + aabbccdd-1122-3344-5566-778899001127: + text: Bws + aabbccdd-1122-3344-5566-778899001128: + text: Trên + pages: + 622a35ec-3795-418a-81f3-a45746959045: + title: Eich enw + 23456789-0abc-def1-2345-67890abcdef1: + title: Llwytho copi o'ch pasbort + 23456789-0abc-def1-2345-67890abcdef2: + title: Llwytho copi o'ch trwydded yrru + da0fbdb4-a2de-4650-be16-9ba552af135f: + title: Dyddiad geni + 449a45f6-4541-4a46-91bd-8b8931b07b50: + title: Crynodeb + 123e4567-e89b-12d3-a456-426614174001: + title: Ble rydych chi'n byw? + aabbccdd-1122-3344-5566-778899001122: + title: Amdanoch chi pages: - title: Your name path: '/your-name' @@ -36,6 +87,21 @@ pages: id: e18116e0-7c3e-416a-af42-6f229017c5b1 next: [] id: 622a35ec-3795-418a-81f3-a45746959045 + - title: Where do you live? + path: '/where-you-live' + components: + - type: UkAddressField + title: What is your address? + name: applicantAddress + shortDescription: Your address + hint: '' + options: + required: true + usePostcodeLookup: true + schema: {} + id: 123e4567-e89b-12d3-a456-426614174000 + next: [] + id: 123e4567-e89b-12d3-a456-426614174001 - title: Upload a copy of your passport controller: FileUploadPageController path: '/upload-passport' @@ -46,7 +112,7 @@ pages: shortDescription: Upload passport hint: '' options: - required: true + required: false schema: {} id: 987c1234-56d7-89e0-1234-56789abcdef0 id: 23456789-0abc-def1-2345-67890abcdef1 @@ -60,7 +126,7 @@ pages: shortDescription: Upload drivers licence hint: '' options: - required: true + required: false schema: {} id: 987c1234-56d7-89e0-1234-56789abcdef1 id: 23456789-0abc-def1-2345-67890abcdef2 @@ -78,6 +144,29 @@ pages: id: '00738799-3489-4ab2-a57b-542eecb31bfa' next: [] id: da0fbdb4-a2de-4650-be16-9ba552af135f + - title: About you + path: '/about-you' + components: + - type: YesNoField + title: Do you have a driving licence? + name: hasDrivingLicence + shortDescription: Driving licence + hint: '' + options: {} + schema: {} + id: aabbccdd-1122-3344-5566-778899001123 + - type: RadiosField + title: How do you usually travel? + name: travelMethod + shortDescription: Your travel method + hint: '' + options: + required: true + schema: {} + list: aabbccdd-1122-3344-5566-778899001125 + id: aabbccdd-1122-3344-5566-778899001124 + next: [] + id: aabbccdd-1122-3344-5566-778899001122 - title: '' path: '/notification-demo' components: @@ -103,4 +192,18 @@ pages: controller: SummaryPageController conditions: [] sections: [] -lists: [] +lists: + - id: aabbccdd-1122-3344-5566-778899001125 + name: travelMethods + title: Travel methods + type: string + items: + - id: aabbccdd-1122-3344-5566-778899001126 + text: Car + value: car + - id: aabbccdd-1122-3344-5566-778899001127 + text: Bus + value: bus + - id: aabbccdd-1122-3344-5566-778899001128 + text: Train + value: train diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 919d7a5d9..1c6a6f208 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -617,7 +617,8 @@ describe('prepareEnvironment', () => { 'checkComponentTemplates', 'checkErrorTemplates', 'evaluate', - 'govukRebrand' + 'govukRebrand', + 't' ] prepareNunjucksEnvironment(mockEnv, mockPluginOptions) diff --git a/src/server/plugins/engine/components/AutocompleteField.test.ts b/src/server/plugins/engine/components/AutocompleteField.test.ts index a94f0574d..080f5a7dc 100644 --- a/src/server/plugins/engine/components/AutocompleteField.test.ts +++ b/src/server/plugins/engine/components/AutocompleteField.test.ts @@ -11,6 +11,7 @@ import { type Field } from '~/src/server/plugins/engine/components/helpers/components.js' import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js' +import { stubTranslator } from '~/src/server/plugins/engine/pageControllers/__stubs__/translator.js' import { listNumber, listNumberExamples, @@ -20,6 +21,10 @@ import { import definition from '~/test/form/definitions/blank.js' import { getFormData, getFormState } from '~/test/helpers/component-helpers.js' +const translator = new FormModel(definition, { + basePath: '/' +}).createTranslator() + describe.each([ { component: { @@ -206,8 +211,8 @@ describe.each([ const state1 = getFormState(item.value) const state2 = getFormState(null) - const answer1 = getAnswer(field, state1) - const answer2 = getAnswer(field, state2) + const answer1 = getAnswer(field, state1, translator) + const answer2 = getAnswer(field, state2, translator) expect(answer1).toBe(item.text) expect(answer2).toBe('') @@ -265,7 +270,11 @@ describe.each([ it('sets Nunjucks component defaults', () => { const item = options.examples[0] - const viewModel = field.getViewModel(getFormData(item.value)) + const viewModel = field.getViewModel({ + payload: getFormData(item.value), + errors: undefined, + translator: stubTranslator + }) expect(viewModel).toEqual( expect.objectContaining({ @@ -280,7 +289,11 @@ describe.each([ it.each([...options.examples])( 'sets Nunjucks component autocomplete suggestions', (item) => { - const viewModel = field.getViewModel(getFormData(item.value)) + const viewModel = field.getViewModel({ + payload: getFormData(item.value), + errors: undefined, + translator: stubTranslator + }) expect(viewModel.items?.[0]).toMatchObject({ value: '' // First item is always empty diff --git a/src/server/plugins/engine/components/AutocompleteField.ts b/src/server/plugins/engine/components/AutocompleteField.ts index 2a9067ff6..312d38ba6 100644 --- a/src/server/plugins/engine/components/AutocompleteField.ts +++ b/src/server/plugins/engine/components/AutocompleteField.ts @@ -1,11 +1,8 @@ import { type AutocompleteFieldComponent } from '@defra/forms-model' import { SelectField } from '~/src/server/plugins/engine/components/SelectField.js' +import { type RenderContext } from '~/src/server/plugins/engine/components/types.js' import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js' -import { - type FormPayload, - type FormSubmissionError -} from '~/src/server/plugins/engine/types.js' export class AutocompleteField extends SelectField { declare options: AutocompleteFieldComponent['options'] @@ -34,8 +31,8 @@ export class AutocompleteField extends SelectField { this.formSchema = formSchema } - getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) { - const viewModel = super.getViewModel(payload, errors) + getViewModel(context: RenderContext) { + const viewModel = super.getViewModel(context) let { formGroup } = viewModel formGroup ??= {} diff --git a/src/server/plugins/engine/components/CheckboxesField.test.ts b/src/server/plugins/engine/components/CheckboxesField.test.ts index 3b3b28189..f2b80974b 100644 --- a/src/server/plugins/engine/components/CheckboxesField.test.ts +++ b/src/server/plugins/engine/components/CheckboxesField.test.ts @@ -13,6 +13,7 @@ import { type Field } from '~/src/server/plugins/engine/components/helpers/components.js' import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js' +import { stubTranslator } from '~/src/server/plugins/engine/pageControllers/__stubs__/translator.js' import { listNumber, listNumberExamples, @@ -22,6 +23,10 @@ import { import definition from '~/test/form/definitions/blank.js' import { getFormData, getFormState } from '~/test/helpers/component-helpers.js' +const translator = new FormModel(definition, { + basePath: '/' +}).createTranslator() + describe.each([ { component: { @@ -304,8 +309,8 @@ describe.each([ const state1 = getFormState([item.value]) const state2 = getFormState(null) - const answer1 = getAnswer(field, state1) - const answer2 = getAnswer(field, state2) + const answer1 = getAnswer(field, state1, translator) + const answer2 = getAnswer(field, state2, translator) expect(answer1).toBe(outdent`