Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
3da1cf1
feat(i18n): finalise en-GB translation file with all template strings
alexluckett Apr 16, 2026
05b0179
feat(i18n): add i18next module with en-GB initialisation and t() wrapper
alexluckett Apr 16, 2026
b6385a5
feat(i18n): add buildValidationMessages factory
alexluckett Apr 16, 2026
475b583
feat(i18n): add language and validationMessages to FormModel
alexluckett Apr 16, 2026
6653669
feat(i18n): rebuild messageTemplate from buildValidationMessages factory
alexluckett Apr 16, 2026
524efd3
feat(i18n): translate DatePartsField and MonthYearField sub-labels
alexluckett Apr 16, 2026
655f133
feat(i18n): translate UkAddressField sub-labels
alexluckett Apr 16, 2026
4174c73
feat(i18n): translate LatLongField and EastingNorthingField strings
alexluckett Apr 16, 2026
e4811d3
feat(i18n): translate LocationFieldBase, NationalGridField, OsGridRef…
alexluckett Apr 17, 2026
a7632e9
feat(i18n): translate DeclarationField and GeospatialField strings
alexluckett Apr 17, 2026
c41db71
feat(i18n): translate PaymentField and FileUploadField runtime strings
alexluckett Apr 17, 2026
2c21615
feat(i18n): translate optionalText in FormComponent.getViewModel
alexluckett Apr 17, 2026
0fd0c4f
feat(i18n): translate SummaryViewModel and SummaryPageController paym…
alexluckett Apr 17, 2026
94e3bca
feat(i18n): translate all RepeatPageController hardcoded strings
alexluckett Apr 17, 2026
1f1cb2c
feat(i18n): register t() as Nunjucks global with en-GB fallback
alexluckett Apr 17, 2026
74bd913
feat(i18n): wire per-request t() into all page controller view models
alexluckett Apr 17, 2026
3fbb5ca
feat(i18n): replace all hardcoded strings in HTML templates with t() …
alexluckett Apr 17, 2026
5083ad3
feat(i18n): fix TypeScript errors — JSON import attribute and Nunjuck…
alexluckett Apr 17, 2026
84d6a4d
Add missing translations
alexluckett Apr 17, 2026
f2362d2
wire in remaining translation keys
alexluckett Apr 17, 2026
065ffc6
move component errors into component block
alexluckett Apr 17, 2026
13d06d0
feat(i18n): add FormModel.t() method — language-bound translation helper
alexluckett Apr 17, 2026
f666c5a
docs(i18n): document FormModel.t() language-binding intent
alexluckett Apr 17, 2026
99db942
refactor(i18n): location field subclasses use model.t() instead of t(…
alexluckett Apr 17, 2026
7ea790e
refactor(i18n): page controllers use model.t() instead of t(key, mode…
alexluckett Apr 17, 2026
0c8c1bf
fix(test): add model.t() to PaymentField dispatcher test mocks
alexluckett Apr 17, 2026
77a8200
Add translation helper in model to reduce repeated language prop access
alexluckett Apr 17, 2026
874f604
refactor(i18n): SummaryViewModel uses model.t() instead of t(key, lang)
alexluckett Apr 17, 2026
14f1ba1
feat(i18n): add pirate translation and simple-form-pirate demo
alexluckett Apr 17, 2026
46f372e
feat(i18n): fully pirate all keys in x-pirate translation
alexluckett Apr 17, 2026
38c602f
fix: revert date part field titles to hardcoded strings
alexluckett Apr 20, 2026
a35bf67
fix: extract payment cancelled banner html to a variable
alexluckett Apr 20, 2026
c02f7bf
sample
alexluckett Apr 20, 2026
22bf3b3
Revert "fix: revert date part field titles to hardcoded strings"
alexluckett Apr 20, 2026
a5d40e5
Revert "fix: extract payment cancelled banner html to a variable"
alexluckett Apr 20, 2026
84404be
feat(i18n): add FormDefinitionTranslations, TContentFunction and Tran…
alexluckett Apr 24, 2026
a62af1b
feat(i18n): add extractBaseTranslations to seed en-GB form namespace
alexluckett Apr 24, 2026
1f7c64b
feat(i18n): add createFormI18nInstance factory for per-FormModel i18n…
alexluckett Apr 24, 2026
75c3572
feat(i18n): add getLanguage callback to PluginOptions
alexluckett Apr 24, 2026
d8fee97
feat(i18n): add createTranslator to FormModel with per-form i18next i…
alexluckett Apr 25, 2026
fb04bb1
fix(i18n): remove dead fallbackLng per-call option; assert exact x-pi…
alexluckett Apr 25, 2026
7b632cb
feat(i18n): store date/month-year sub-field titles as i18next key con…
alexluckett Apr 25, 2026
3be964a
test(i18n): annotate key-constant-in-error-message assertions as temp…
alexluckett Apr 25, 2026
104e0b1
feat(i18n): store address/location sub-field titles as i18next key co…
alexluckett Apr 25, 2026
cc33258
fix(i18n): remove out-of-scope language field from UkAddressField dis…
alexluckett Apr 25, 2026
e5fb12f
feat(i18n): add language field to adapter meta payload
alexluckett Apr 25, 2026
41cc075
feat(i18n): FormComponent.getViewModel accepts Translator; fix tConte…
alexluckett Apr 25, 2026
659bc8d
feat(i18n): wire Translator through ComponentCollection, FileUploadFi…
alexluckett Apr 25, 2026
247d801
feat(i18n): postcode-lookup language support and i18n text extraction
alexluckett Apr 25, 2026
dc6fa12
feat(i18n): ComponentCollection.validate accepts Translator for per-r…
alexluckett Apr 25, 2026
08e3368
feat(i18n): PageController and QuestionPageController use per-request…
alexluckett Apr 25, 2026
0788997
feat(i18n): RepeatPageController uses per-request translator instead …
alexluckett Apr 25, 2026
37e148c
feat(i18n): SummaryViewModel and SummaryPageController use per-reques…
alexluckett Apr 27, 2026
f95eb6d
feat(i18n): wire per-request Translator through DeclarationField, Fil…
alexluckett Apr 27, 2026
a341d1f
feat(i18n): PaymentField.dispatcher and onSubmit use per-request tran…
alexluckett Apr 27, 2026
2ef58a2
feat(i18n): thread Translator through getFormContext, validateFormPay…
alexluckett Apr 27, 2026
d891f7d
feat(i18n): replace model.validationMessages with static messageTempl…
alexluckett Apr 27, 2026
9e62b11
feat(i18n): remove model.t/language/validationMessages; require Trans…
alexluckett Apr 27, 2026
8d92f3c
add welsh devtool simple-form
alexluckett Apr 27, 2026
ba14b07
feat(i18n): eliminate Translator|FormQuery union; thread translator t…
alexluckett Apr 27, 2026
5cb37e2
feat(i18n): add Welsh (cy) translations
alexluckett Apr 28, 2026
d7a18b0
fix(i18n): apply translated shortDescription as Joi label at validati…
alexluckett Apr 28, 2026
bbae7e0
fix(i18n): use translated shortDescription and title in summary table…
alexluckett Apr 29, 2026
c296543
feat(i18n): persist language preference in session; register t as nun…
alexluckett Apr 29, 2026
6273811
fix(i18n): thread translator through FileUploadField.getDisplayString…
alexluckett Apr 29, 2026
a64a82e
fix(i18n): translate composite sub-field labels/messages and Declarat…
alexluckett Apr 29, 2026
477e39d
fix(i18n): pass language to postcode lookup dispatch so journey rende…
alexluckett Apr 29, 2026
2bac7ee
fix(i18n): translate YesNo/list items, DeclarationField validation, s…
alexluckett Apr 30, 2026
0c06899
add welsh devtool about-you page with YesNo and radios demo
alexluckett Apr 30, 2026
d02cf8b
fix(i18n): resolve plugin i18n keys for list items with GUIDs; transl…
alexluckett Apr 30, 2026
d22e359
refactor(i18n): encapsulate translator resolution in PageController; …
alexluckett Apr 30, 2026
db5ca66
swap translation functions with separates per type
alexluckett Apr 30, 2026
fe7a9fc
tidy up signatures
alexluckett Apr 30, 2026
e24d5a0
refactor(i18n): make translator required on all rendering methods; el…
alexluckett Apr 30, 2026
d15fc68
Stash for rebase
jbarnsley10 Jun 24, 2026
bdf42dc
Merge branch 'main' into feature/i18n-linting
jbarnsley10 Jun 24, 2026
e1570a7
Stash
jbarnsley10 Jun 24, 2026
fc6e221
Fixed types
jbarnsley10 Jun 25, 2026
555ba02
Temp stash with only 3 test suites failing
jbarnsley10 Jun 25, 2026
56e9023
Further test fixes
jbarnsley10 Jun 25, 2026
e5fcfc1
Consolidated language methods
jbarnsley10 Jun 26, 2026
0a21a64
Fixed code/test
jbarnsley10 Jun 29, 2026
804ae6a
Fixed auto-doc generation + added remain welsh translations
jbarnsley10 Jun 29, 2026
ca810df
Merge branch 'main' into feature/i18n-linting
jbarnsley10 Jun 29, 2026
1ac4b11
Sonar fixes - part 1
jbarnsley10 Jun 29, 2026
86efc3a
Prettified
jbarnsley10 Jun 29, 2026
58550af
Sonar fixes (Remove nested ternary)
davidjamesstone Jun 29, 2026
389f9ef
Form root-level translations + language property
jbarnsley10 Jun 30, 2026
b540fec
Merge branch 'feature/i18n-linting' of https://github.com/DEFRA/forms…
jbarnsley10 Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions jest.setup.cjs
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
1,419 changes: 777 additions & 642 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion scripts/generate-component-previews.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
31 changes: 23 additions & 8 deletions scripts/generate-component-previews.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -132,6 +133,10 @@ describe('buildPartialMdx', () => {
describe('renderComponent', () => {
let mockGetViewModel

const mockModel = {
createTranslator: () => stubTranslator
}

beforeEach(() => {
mockGetViewModel = jest.fn().mockReturnValue({
type: 'TextField',
Expand All @@ -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({
Expand Down
30 changes: 21 additions & 9 deletions scripts/page-preview-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} */ (
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())
)
)
}
})
Expand Down
14 changes: 0 additions & 14 deletions src/server/forms/register-as-a-unicorn-breeder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
80 changes: 80 additions & 0 deletions src/server/forms/simple-form-pirate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: Pirate Form
engine: V2
schema: 2
startPage: '/summary'
metadata:
language: x-pirate
pages:
- title: Yer name, landlubber
path: '/your-name'
components:
- type: TextField
title: What be yer first name?
name: applicantFirstName
shortDescription: Yer first name
hint: ''
options:
required: true
schema: {}
id: 1fb8e182-c709-4792-8f83-e01d8b1fee1a
- type: TextField
title: What be yer last name?
name: applicantLastName
shortDescription: Yer last name
hint: ''
options:
required: true
schema: {}
id: b68df7f1-d4f4-4c17-83c8-402f584906c9
next: []
id: 622a35ec-3795-418a-81f3-a45746959045
- title: Hoist yer passport
controller: FileUploadPageController
path: '/upload-passport'
components:
- type: FileUploadField
title: Hoist a copy of yer passport, arrr
name: passportUpload
shortDescription: Hoist passport
hint: ''
options:
required: false
schema: {}
id: 987c1234-56d7-89e0-1234-56789abcdef0
id: 23456789-0abc-def1-2345-67890abcdef1
- title: Hoist yer ship's licence
controller: FileUploadPageController
path: '/upload-driving-licence'
components:
- type: FileUploadField
title: Hoist a copy of yer drivers licence, matey
name: driversLicenceUpload
shortDescription: Hoist drivers licence
hint: ''
options:
required: false
schema: {}
id: 987c1234-56d7-89e0-1234-56789abcdef1
id: 23456789-0abc-def1-2345-67890abcdef2
- title: ''
path: '/date-of-birth'
components:
- type: DatePartsField
title: When did {{ applicantFirstName }} {{ applicantLastName }} first set sail?
name: dateOfBirth
shortDescription: Yer birthday
hint: ''
options:
required: true
schema: {}
id: '00738799-3489-4ab2-a57b-542eecb31bfa'
next: []
id: da0fbdb4-a2de-4650-be16-9ba552af135f
- id: 449a45f6-4541-4a46-91bd-8b8931b07b50
title: ''
path: '/summary'
controller: SummaryPageController
conditions: []
sections: []
lists: []
Loading
Loading