From 81dc6d94b07a14bb9e9273afb55a7d3cd639818e Mon Sep 17 00:00:00 2001 From: Alejandro Guzman Date: Fri, 24 Jul 2026 19:18:05 -0500 Subject: [PATCH] 5758 - Improve error tooltip text formatting --- src/i18n/resources/en/generalValidation.json | 2 +- src/meta/assessment/validation/validation.ts | 10 +++++++++- .../functions/NWFPProductHasCategory.ts | 2 +- .../functions/validatorEqualToSum.ts | 15 +++++++++++++-- .../functions/validatorNWFPProductAndCategory.ts | 2 +- src/meta/validations/messageParser/getMessage.ts | 12 +++++++++--- src/meta/validations/messageParser/utils.ts | 12 ++++++++++-- .../verifyLinks/utils/getLinkValidationMessage.ts | 2 +- src/test/e2e/utils/links/index.ts | 4 ++-- 9 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/i18n/resources/en/generalValidation.json b/src/i18n/resources/en/generalValidation.json index f1076c8fc2..993f9875e2 100644 --- a/src/i18n/resources/en/generalValidation.json +++ b/src/i18n/resources/en/generalValidation.json @@ -9,7 +9,7 @@ "forestAreaReportedIsDifferentFromPreviousCycle": "Reported area ({{forestArea2025}}) differs from previously reported ({{forestArea2020}}). Please provide a comment explaining why it is different.", "forestSumAreaExceedsExtentOfForest": "The sum of the reported FRA categories exceeds the forest area reported in table 1a", "invalidLink": "Invalid link", - "invalidLinkWithReason": "Invalid link: \"{{- link}}\" ({{reason}})", + "invalidLinkWithReason": "Invalid link: \"{{link}}\" ({{reason}})", "landAreaExceedsTotalLandArea": "Land area exceeds total land area (1a) ({{value}})", "mustBeEqualToForestArea": "Sum of Naturally regenerating forest and Planted forest not equal to the total forest", "mustBeEqualToForestExpansion": "Subcategories sum should be equal to Forest Expansion", diff --git a/src/meta/assessment/validation/validation.ts b/src/meta/assessment/validation/validation.ts index bce3a493dc..8b56b36fb3 100644 --- a/src/meta/assessment/validation/validation.ts +++ b/src/meta/assessment/validation/validation.ts @@ -1,4 +1,12 @@ -export type ValidationMessageParam = string | number | Array | Array +export type ValidationMessageParamKey = { key: string } + +export type ValidationMessageParam = + | string + | number + | ValidationMessageParamKey + | Array + | Array + | Array export interface ValidationMessage { name?: string diff --git a/src/meta/expressionEvaluator/functions/NWFPProductHasCategory.ts b/src/meta/expressionEvaluator/functions/NWFPProductHasCategory.ts index 0e151aee84..cdac2da840 100644 --- a/src/meta/expressionEvaluator/functions/NWFPProductHasCategory.ts +++ b/src/meta/expressionEvaluator/functions/NWFPProductHasCategory.ts @@ -19,7 +19,7 @@ export const NWFPProductHasCategory: ExpressionFunction = { { name: ValidatorName.nwfpProductHasCategory, key: 'generalValidation.columnEmpty', - params: { columName: 'nonWoodForestProductsRemovals.category' }, + params: { columName: { key: 'nonWoodForestProductsRemovals.category' } }, }, ] diff --git a/src/meta/expressionEvaluator/functions/validatorEqualToSum.ts b/src/meta/expressionEvaluator/functions/validatorEqualToSum.ts index 61891970e0..b2c56a75a2 100644 --- a/src/meta/expressionEvaluator/functions/validatorEqualToSum.ts +++ b/src/meta/expressionEvaluator/functions/validatorEqualToSum.ts @@ -1,4 +1,5 @@ import { NodeValueValidation, NodeValueValidationMessage } from 'meta/assessment/nodeValueValidation' +import { ValidationMessageParam, ValidationMessageParamKey } from 'meta/assessment/validation/validation' import { ValidatorName } from 'meta/expressionEvaluator/validatorName' import { Numbers } from 'utils/numbers' import { Objects } from 'utils/objects' @@ -17,12 +18,16 @@ export const validatorEqualToSum: ExpressionFunction = { parentVariable = 'parent', col = '', table = '', - subcategories = '' + subcategories: string | Array = '' ): NodeValueValidation => { const valid = Objects.isEmpty(value) || Numbers.eqWithTolerance(value, Numbers.sum(otherValues?.filter((v) => !Objects.isEmpty(v)))) + const subcategoryLabels: ValidationMessageParam = Array.isArray(subcategories) + ? subcategories.map((subcategory) => ({ key: subcategory })) + : subcategories + const valueRounded = parseFloat(value).toFixed(2) const messages: Array = valid ? undefined @@ -30,7 +35,13 @@ export const validatorEqualToSum: ExpressionFunction = { { name: ValidatorName.equalToSum, key: 'generalValidation.valueEqualToSumParent', - params: { parentVariable, subcategories, parentCol: col, parentTable: table, valueRounded }, + params: { + parentVariable: { key: parentVariable }, + subcategories: subcategoryLabels, + parentCol: { key: col }, + parentTable: table, + valueRounded, + }, }, ] diff --git a/src/meta/expressionEvaluator/functions/validatorNWFPProductAndCategory.ts b/src/meta/expressionEvaluator/functions/validatorNWFPProductAndCategory.ts index 27938f8305..1694847a72 100644 --- a/src/meta/expressionEvaluator/functions/validatorNWFPProductAndCategory.ts +++ b/src/meta/expressionEvaluator/functions/validatorNWFPProductAndCategory.ts @@ -19,7 +19,7 @@ export const validatorNWFPProductAndCategory: ExpressionFunction = { { name: ValidatorName.nwfpProductAndCategory, key: 'generalValidation.columnEmpty', - params: { columName: 'nonWoodForestProductsRemovals.category' }, + params: { columName: { key: 'nonWoodForestProductsRemovals.category' } }, }, ] diff --git a/src/meta/validations/messageParser/getMessage.ts b/src/meta/validations/messageParser/getMessage.ts index 0e179d4872..150346e2c1 100644 --- a/src/meta/validations/messageParser/getMessage.ts +++ b/src/meta/validations/messageParser/getMessage.ts @@ -8,6 +8,12 @@ import { parseSumEqualTo, SumEqualToParams } from './sumEqualTo' import { parseSumSubCategories, SumSubCategoriesParams } from './sumSubCategories' import { translateParams } from './utils' +// Validation messages are rendered as plain text (tooltips), never as HTML, so i18next +// escaping is disabled here. Otherwise params would turn into HTML entities, e.g a national +// class tooltip would show "Production & protection" instead of "Production & protection" +const _translateMessage = (t: TFunction, key: string, params?: Record): string => + t(key, { ...params, interpolation: { escapeValue: false } }) + export const getMessage = (t: TFunction, message: ValidationMessage): string => { const { key, params } = message @@ -16,15 +22,15 @@ export const getMessage = (t: TFunction, message: ValidationMessage): string => } if (message.name === ValidatorName.sumEqualTo) { - return t(key, parseSumEqualTo(t, params as SumEqualToParams)) + return _translateMessage(t, key, parseSumEqualTo(t, params as SumEqualToParams)) } if ( message.name === ValidatorName.sumSubCategoriesNotEqualToParent || message.name === ValidatorName.sumSubCategoriesNotGreaterThanParent ) { - return t(key, parseSumSubCategories(t, params as SumSubCategoriesParams)) + return _translateMessage(t, key, parseSumSubCategories(t, params as SumSubCategoriesParams)) } - return t(key, translateParams(t, params)) + return _translateMessage(t, key, translateParams(t, params)) } diff --git a/src/meta/validations/messageParser/utils.ts b/src/meta/validations/messageParser/utils.ts index 29420f53fb..8522a99fb9 100644 --- a/src/meta/validations/messageParser/utils.ts +++ b/src/meta/validations/messageParser/utils.ts @@ -3,12 +3,20 @@ import { TFunction } from 'i18next' import { ValidationMessageParam } from 'meta/assessment/validation/validation' import { Objects } from 'utils/objects' +// Only params wrapped in { key } are translated. The rest is user data (URLs, names, numbers) +// that t(...) would corrupt: i18next treats ":" as a namespace separator, +// e.g t("https://link.com") -> "//link.com" export const translateParam = (t: TFunction, param: ValidationMessageParam): string => { if (Array.isArray(param)) { - return `(${param.map((item) => translateParam(t, item)).join(', ')})` + return `(${param.map((item) => translateParam(t, item)).join(', ')})` } - return t(String(param)) + const isTranslationKey = !Objects.isNil(param) && typeof param === 'object' + if (isTranslationKey) { + return t(param.key) + } + + return String(param) } export const translateParams = ( diff --git a/src/server/worker/tasks/verifyLinks/utils/getLinkValidationMessage.ts b/src/server/worker/tasks/verifyLinks/utils/getLinkValidationMessage.ts index 5669ea3c7f..ae8a0e60e3 100644 --- a/src/server/worker/tasks/verifyLinks/utils/getLinkValidationMessage.ts +++ b/src/server/worker/tasks/verifyLinks/utils/getLinkValidationMessage.ts @@ -27,7 +27,7 @@ export const getLinkValidationMessage = (props: Props): ValidationMessage | unde key: 'generalValidation.invalidLinkWithReason', params: { link: invalidLinkLabel, - reason: Links.getI18nValidationStatusLabelKey(validationCode), + reason: { key: Links.getI18nValidationStatusLabelKey(validationCode) }, }, } } diff --git a/src/test/e2e/utils/links/index.ts b/src/test/e2e/utils/links/index.ts index a264f4019a..f663b6998b 100644 --- a/src/test/e2e/utils/links/index.ts +++ b/src/test/e2e/utils/links/index.ts @@ -17,8 +17,8 @@ const buildInvalidLinksHtml = (label: string): InvalidLinksFixture => { const emptyLinkText = `empty link ${label}` const brokenLinkText = `broken link ${label}` const brokenLinkDomain = `${label}.this-domain-does-not-exist-e2e-test.invalid` - const brokenLinkDisplayUrl = `//${brokenLinkDomain}` - const html = `${emptyLinkText}
${brokenLinkText}` + const brokenLinkDisplayUrl = `https://${brokenLinkDomain}` + const html = `${emptyLinkText}
${brokenLinkText}` return { brokenLinkDisplayUrl, brokenLinkText, emptyLinkText, html } }