|
| 1 | +# ADR044: Translate API response at request time |
| 2 | + |
| 3 | +Date: 2025-08-01 |
| 4 | + |
| 5 | +## Status |
| 6 | + |
| 7 | +Accepted |
| 8 | +## Context |
| 9 | + |
| 10 | +Our current API is based around form documents which contain all the information forms-runner needs to render a form: |
| 11 | + |
| 12 | +```jsonc |
| 13 | +{ |
| 14 | + "form_id": "7", |
| 15 | + "name": "A form for testing file upload", |
| 16 | + "submission_email": "david.biddle@digital.cabinet-office.gov.uk", |
| 17 | + "privacy_policy_url": null, |
| 18 | + "form_slug": "a-form-for-testing-file-upload", |
| 19 | + "support_email": null, |
| 20 | + "support_phone": null, |
| 21 | + "support_url": null, |
| 22 | + "support_url_text": null, |
| 23 | + "declaration_text": null, |
| 24 | + "question_section_completed": false, |
| 25 | + "declaration_section_completed": false, |
| 26 | + "created_at": "2025-03-27T12:05:49.395300Z", |
| 27 | + "updated_at": "2025-07-29T14:35:44.423406Z", |
| 28 | + "creator_id": 1, |
| 29 | + "what_happens_next_markdown": null, |
| 30 | + "payment_url": null, |
| 31 | + "submission_type": "email", |
| 32 | + "share_preview_completed": false, |
| 33 | + "s3_bucket_name": null, |
| 34 | + "s3_bucket_aws_account_id": null, |
| 35 | + "s3_bucket_region": null, |
| 36 | + "language": "en", |
| 37 | + "start_page": 33, |
| 38 | + "steps": [ |
| 39 | + { |
| 40 | + "id": 33, |
| 41 | + "position": 1, |
| 42 | + "next_step_id": null, |
| 43 | + "type": "question_page", |
| 44 | + "data": { |
| 45 | + "question_text": "Upload your file", |
| 46 | + "hint_text": "", |
| 47 | + "answer_type": "file", |
| 48 | + "is_optional": false, |
| 49 | + "answer_settings": null, |
| 50 | + "page_heading": "Annual report", |
| 51 | + "guidance_markdown": "Some guidance", |
| 52 | + "is_repeatable": false |
| 53 | + }, |
| 54 | + "routing_conditions": [] |
| 55 | + } |
| 56 | + ] |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +A form can have multiple from documents associated with it, depending on its state: |
| 61 | +```jsonc |
| 62 | +{ |
| 63 | + "id": "7", |
| 64 | + "links": { |
| 65 | + "self": "/api/v2/forms/7", |
| 66 | + "draft": "/api/v2/forms/7/draft", |
| 67 | + "live": "/api/v2/forms/7/live", |
| 68 | + // or |
| 69 | + "archived": "/api/v2/forms/7/archived" |
| 70 | + } |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +Currently form documents only contain English language text, but we want to add Welsh translations for form-filler facing text. |
| 75 | + |
| 76 | +There are a few ways we could model this. |
| 77 | +### 1. Adding additional fields to the form document |
| 78 | + |
| 79 | +One option is to add extra keys to the existing form document with the new translations. We could do this by renaming the existing field with an `_en` suffix and adding a new key with a `_cy` suffix. This is roughly what the mobility gem does with its database columns: |
| 80 | +```jsonc |
| 81 | +{ |
| 82 | + "form_id": "7", |
| 83 | + "name_en": "A form for testing file upload", |
| 84 | + "name_cy": "Ffurflen ar gyfer profi uwchlwytho ffeiliau", |
| 85 | + "submission_email": "david.biddle@digital.cabinet-office.gov.uk", |
| 86 | + "privacy_policy_url": null, |
| 87 | + "form_slug": "a-form-for-testing-file-upload", |
| 88 | + "support_email": null, |
| 89 | + "support_phone": null, |
| 90 | + "support_url": null, |
| 91 | + "support_url_text": null, |
| 92 | + "declaration_text": null, |
| 93 | + "question_section_completed": false, |
| 94 | + "declaration_section_completed": false, |
| 95 | + "created_at": "2025-03-27T12:05:49.395300Z", |
| 96 | + "updated_at": "2025-07-29T14:35:44.423406Z", |
| 97 | + "creator_id": 1, |
| 98 | + "what_happens_next_markdown": null, |
| 99 | + "payment_url": null, |
| 100 | + "submission_type": "email", |
| 101 | + "share_preview_completed": false, |
| 102 | + "s3_bucket_name": null, |
| 103 | + "s3_bucket_aws_account_id": null, |
| 104 | + "s3_bucket_region": null, |
| 105 | + "language": "en", |
| 106 | + "start_page": 33, |
| 107 | + "steps": [ |
| 108 | + { |
| 109 | + "id": 33, |
| 110 | + "position": 1, |
| 111 | + "next_step_id": null, |
| 112 | + "type": "question_page", |
| 113 | + "data": { |
| 114 | + "question_text_en": "Upload your file", |
| 115 | + "question_text_cy": "Llwythwch eich ffeil i fyny", |
| 116 | + "hint_text": "", |
| 117 | + "answer_type": "file", |
| 118 | + "is_optional": false, |
| 119 | + "answer_settings": null, |
| 120 | + "page_heading_en": "Annual report", |
| 121 | + "page_heading_cy": "Adroddiad blynyddol", |
| 122 | + "guidance_markdown_en": "Some guidance", |
| 123 | + "guidance_markdown_cy": "Peth arweiniad", |
| 124 | + "is_repeatable": false |
| 125 | + }, |
| 126 | + "routing_conditions": [] |
| 127 | + } |
| 128 | + ] |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +We could also keep the existing keys as they are, and add a new 'translations' object containing only the Welsh translations for the relevant field: |
| 133 | +```jsonc |
| 134 | +// or: |
| 135 | +{ |
| 136 | + "form_id": "7", |
| 137 | + "name": "A form for testing file upload", |
| 138 | + "submission_email": "david.biddle@digital.cabinet-office.gov.uk", |
| 139 | + "privacy_policy_url": null, |
| 140 | + "form_slug": "a-form-for-testing-file-upload", |
| 141 | + "support_email": null, |
| 142 | + "support_phone": null, |
| 143 | + "support_url": null, |
| 144 | + "support_url_text": null, |
| 145 | + "declaration_text": null, |
| 146 | + "question_section_completed": false, |
| 147 | + "declaration_section_completed": false, |
| 148 | + "created_at": "2025-03-27T12:05:49.395300Z", |
| 149 | + "updated_at": "2025-07-29T14:35:44.423406Z", |
| 150 | + "creator_id": 1, |
| 151 | + "what_happens_next_markdown": null, |
| 152 | + "payment_url": null, |
| 153 | + "submission_type": "email", |
| 154 | + "share_preview_completed": false, |
| 155 | + "s3_bucket_name": null, |
| 156 | + "s3_bucket_aws_account_id": null, |
| 157 | + "s3_bucket_region": null, |
| 158 | + "language": "en", |
| 159 | + "start_page": 33, |
| 160 | + "translations": { |
| 161 | + "cy": { |
| 162 | + "name": "Ffurflen ar gyfer profi uwchlwytho ffeiliau", |
| 163 | + } |
| 164 | + }, |
| 165 | + "steps": [ |
| 166 | + { |
| 167 | + "id": 33, |
| 168 | + "position": 1, |
| 169 | + "next_step_id": null, |
| 170 | + "type": "question_page", |
| 171 | + "data": { |
| 172 | + "question_text": "Upload your file", |
| 173 | + "hint_text": "", |
| 174 | + "answer_type": "file", |
| 175 | + "is_optional": false, |
| 176 | + "answer_settings": null, |
| 177 | + "page_heading": "Annual report", |
| 178 | + "guidance_markdown": "Some guidance", |
| 179 | + "is_repeatable": false |
| 180 | + }, |
| 181 | + "translations": { |
| 182 | + "cy": { |
| 183 | + "question_text": "Llwythwch eich ffeil i fyny", |
| 184 | + "page_heading": "Adroddiad blynyddol", |
| 185 | + "guidance_markdown": "Peth arweiniad", |
| 186 | + } |
| 187 | + }, |
| 188 | + "routing_conditions": [] |
| 189 | + } |
| 190 | + ] |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +Both cases are essentially the same. |
| 195 | +Benefits: |
| 196 | +- Few admin/api changes required |
| 197 | +Disadvantages: |
| 198 | +- More runner logic, since we'd have to manage choosing the right translation and fallback behaviour in runner. |
| 199 | +- We'd get less benefit from the Mobility gem - we'd be using it for managing the columns and dealing with translations in admin, but we wouldn't be using its querying or fallback features in forms-runner. |
| 200 | +- For forms with lots of content (e.g. multiple pages with a large amount of detailed guidance or what happens next text), this would increase the size of the request quite a lot since this information would be present in two different languages. Much of that content wouldn't be used for a given request, since the runner only shows one language at a time. |
| 201 | +- Additional languages would make it even more verbose. |
| 202 | + |
| 203 | +### 2. Selecting languages at request time |
| 204 | +In this approach, the form snapshot in the database would look the same as in the above approach. But we would allow the runner to request a specific language when requesting the form from the API, and the API would only supply translations for the requested language. |
| 205 | + |
| 206 | +So `/forms/7/live?locale=en` would return: |
| 207 | +```jsonc |
| 208 | +{ |
| 209 | + "form_id": "7", |
| 210 | + "name": "A form for testing file upload", |
| 211 | + "submission_email": "david.biddle@digital.cabinet-office.gov.uk", |
| 212 | + "privacy_policy_url": null, |
| 213 | + "form_slug": "a-form-for-testing-file-upload", |
| 214 | + "support_email": null, |
| 215 | + "support_phone": null, |
| 216 | + "support_url": null, |
| 217 | + "support_url_text": null, |
| 218 | + "declaration_text": null, |
| 219 | + "question_section_completed": false, |
| 220 | + "declaration_section_completed": false, |
| 221 | + "created_at": "2025-03-27T12:05:49.395300Z", |
| 222 | + "updated_at": "2025-07-29T14:35:44.423406Z", |
| 223 | + "creator_id": 1, |
| 224 | + "what_happens_next_markdown": null, |
| 225 | + "payment_url": null, |
| 226 | + "submission_type": "email", |
| 227 | + "share_preview_completed": false, |
| 228 | + "s3_bucket_name": null, |
| 229 | + "s3_bucket_aws_account_id": null, |
| 230 | + "s3_bucket_region": null, |
| 231 | + "language": "en", |
| 232 | + "start_page": 33, |
| 233 | + "steps": [ |
| 234 | + { |
| 235 | + "id": 33, |
| 236 | + "position": 1, |
| 237 | + "next_step_id": null, |
| 238 | + "type": "question_page", |
| 239 | + "data": { |
| 240 | + "question_text": "Upload your file", |
| 241 | + "hint_text": "", |
| 242 | + "answer_type": "file", |
| 243 | + "is_optional": false, |
| 244 | + "answer_settings": null, |
| 245 | + "page_heading": "Annual report", |
| 246 | + "guidance_markdown": "Some guidance", |
| 247 | + "is_repeatable": false |
| 248 | + }, |
| 249 | + "routing_conditions": [] |
| 250 | + } |
| 251 | + ] |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +And `/forms/7/live?locale=cy`, would return the same object with any Welsh translations overwriting the English: |
| 256 | +```jsonc |
| 257 | +{ |
| 258 | + "form_id": "7", |
| 259 | + "name": "Ffurflen ar gyfer profi uwchlwytho ffeiliau", |
| 260 | + "submission_email": "david.biddle@digital.cabinet-office.gov.uk", |
| 261 | + "privacy_policy_url": null, |
| 262 | + "form_slug": "a-form-for-testing-file-upload", |
| 263 | + "support_email": null, |
| 264 | + "support_phone": null, |
| 265 | + "support_url": null, |
| 266 | + "support_url_text": null, |
| 267 | + "declaration_text": null, |
| 268 | + "question_section_completed": false, |
| 269 | + "declaration_section_completed": false, |
| 270 | + "created_at": "2025-03-27T12:05:49.395300Z", |
| 271 | + "updated_at": "2025-07-29T14:35:44.423406Z", |
| 272 | + "creator_id": 1, |
| 273 | + "what_happens_next_markdown": null, |
| 274 | + "payment_url": null, |
| 275 | + "submission_type": "email", |
| 276 | + "share_preview_completed": false, |
| 277 | + "s3_bucket_name": null, |
| 278 | + "s3_bucket_aws_account_id": null, |
| 279 | + "s3_bucket_region": null, |
| 280 | + "language": "en", |
| 281 | + "start_page": 33, |
| 282 | + "steps": [ |
| 283 | + { |
| 284 | + "id": 33, |
| 285 | + "position": 1, |
| 286 | + "next_step_id": null, |
| 287 | + "type": "question_page", |
| 288 | + "data": { |
| 289 | + "question_text": "Llwythwch eich ffeil i fyny", |
| 290 | + "hint_text": "", |
| 291 | + "answer_type": "file", |
| 292 | + "is_optional": false, |
| 293 | + "answer_settings": null, |
| 294 | + "page_heading": "Adroddiad blynyddol", |
| 295 | + "guidance_markdown": "Peth arweiniad", |
| 296 | + "is_repeatable": false |
| 297 | + }, |
| 298 | + "routing_conditions": [] |
| 299 | + } |
| 300 | + ] |
| 301 | +} |
| 302 | +``` |
| 303 | + |
| 304 | +Advantages: |
| 305 | +- Request size doesn't increase |
| 306 | +- API structure doesn't change aside from the addition of a query parameter. |
| 307 | +- Keeps the translation logic out of runner |
| 308 | +Disadvantages: |
| 309 | +- Requires more logic in the API to choose the right translation - we already have some conversion logic, but we're currently trying to reduce the amount of logic in this repo. |
| 310 | +- API is doing more work at request time, which might slow down its responses for forms with lots of questions. |
| 311 | +- Small runner change required to request the correct version |
| 312 | + |
| 313 | +### 3. Creating separate Welsh and English snapshots when making a form live |
| 314 | +We could create separate snapshots for Welsh and English when making a form live - so the API would store two separate form documents, one in English and one in Welsh. At request time, the runner would request a form document and request a language, and API would return the relevant version of the form document. The objects returned would be identical to those in the 'Selecting languages at request time' approach. |
| 315 | + |
| 316 | +Advantages: |
| 317 | +- API structure doesn't change aside from the addition of a query parameter. |
| 318 | +- All translation logic including fallbacks can be handled within admin, API/runner don't have to do any processing on the resulting form documents - just need to serve/request the right version. |
| 319 | +- No additional processing or conversion would be required at request time, just returning an existing form document. |
| 320 | +Disadvantages: |
| 321 | +- More complexity in forms-admin. Probably needs API version increment. |
| 322 | +- Potential for overlap with API team's work. |
| 323 | +- Small runner change required to request the correct version. |
| 324 | +- Having two separate documents might risk the two versions going out of sync unexpectedly. |
| 325 | +- Would need to think about how we store the separate form documents - having them in one database column might be hard to query. |
| 326 | + |
| 327 | +## Decision |
| 328 | + |
| 329 | +Go with 'Selecting languages at request time' for now. Consider moving to 'Creating separate Welsh and English snapshots when making a form live' approach later if we add other languages and this this will be useful. |
| 330 | + |
| 331 | +## Consequences |
| 332 | +Changes to forms-runner should be minimal, limited to making a request for the correct locale. |
| 333 | + |
| 334 | +If we change to the 'Creating separate Welsh and English snapshots when making a form live' approach later we shouldn't need to make any runner updates. |
| 335 | + |
| 336 | +We'll need to keep an eye on API response times to make sure they aren't having an impact on the performance of our service. |
0 commit comments