From f2f36dec93bd054545c1d821df72f30fba0c116b Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:24:03 +0000 Subject: [PATCH 001/118] feat: add custom plural for note relationship attribute --- i18n/en.pot | 4 +- .../helpers/customLabels/customLabels.ts | 44 ++++++++++--------- .../TrackedEntityTypeFactory.ts | 5 ++- .../quickStoreOperations/storePrograms.ts | 3 ++ .../types/apiPrograms.types.ts | 3 ++ .../storageControllers/types/cache.types.ts | 3 ++ 6 files changed, 38 insertions(+), 24 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d04cbca43f..9b3d4b0088 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-07-21T13:35:51.419Z\n" -"PO-Revision-Date: 2026-07-21T13:35:51.419Z\n" +"POT-Creation-Date: 2026-07-23T08:24:04.879Z\n" +"PO-Revision-Date: 2026-07-23T08:24:04.880Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 18938840cd..0366c64f64 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,37 +1,40 @@ +import { capitalizeFirstLetter } from 'capture-core-utils/string'; + type CustomLabelField = { - field?: string, - pluralField?: string, + singular?: string, + plural?: string, }; export const CUSTOM_LABEL_FIELDS = { - enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, - followUp: { field: 'displayFollowUpLabel' }, - orgUnit: { field: 'displayOrgUnitLabel' }, - relationship: { field: 'displayRelationshipLabel' }, - note: { field: 'displayNoteLabel' }, - attribute: { field: 'displayTrackedEntityAttributeLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel' }, + enrollment: { singular: 'displayEnrollmentLabel', plural: 'displayEnrollmentsLabel' }, + followUp: { singular: 'displayFollowUpLabel' }, + orgUnit: { singular: 'displayOrgUnitLabel' }, + note: { singular: 'displayNoteLabel', plural: 'displayNotesLabel' }, + relationship: { singular: 'displayRelationshipLabel', plural: 'displayRelationshipsLabel' }, + attribute: { singular: 'displayTrackedEntityAttributeLabel', plural: 'displayTrackedEntityAttributesLabel' }, + programStage: { singular: 'displayProgramStageLabel', plural: 'displayProgramStagesLabel' }, + event: { singular: 'displayEventLabel', plural: 'displayEventsLabel' }, + trackedEntityType: { singular: 'displayTrackedEntityTypeLabel', plural: 'displayTrackedEntityTypesLabel' }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; export type CustomLabels = Record; export type LabelOptions = { plural?: boolean }; -const allFields: Array = Array.from( +const ALL_FIELDS: ReadonlyArray = Array.from( new Set( Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) + .flatMap((term: CustomLabelField) => [term.singular, term.plural]) .filter((field): field is string => Boolean(field)), ), ); -export const extractCustomLabels = (cached: Record): CustomLabels => { +export const extractCustomLabels = (cached: Record): CustomLabels => { const labels: CustomLabels = {}; - allFields.forEach((field) => { - if (cached[field]) { - labels[field] = cached[field]; + ALL_FIELDS.forEach((field) => { + const value = cached[field]; + if (typeof value === 'string' && value) { + labels[field] = value; } }); return labels; @@ -48,10 +51,9 @@ export const resolveLabel = ( const list = Array.isArray(sources) ? sources : [sources]; const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); - if (plural) { - return term.pluralField ? pick(term.pluralField) : pick(term.field); - } - return pick(term.field); + const field = plural && term.plural ? term.plural : term.singular; + const value = pick(field); + return value ? capitalizeFirstLetter(value) : value; }; type WithLabels = { customLabels?: CustomLabels } | undefined | null; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index 7e5e9d5a91..3abe925f6e 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -87,7 +87,10 @@ export class TrackedEntityTypeFactory { o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.customLabels = extractCustomLabels(cachedType); + o.customLabels = extractCustomLabels({ + ...cachedType, + displayTrackedEntityTypeLabel: cachedType.displayName, + }); }); if (cachedType.trackedEntityTypeAttributes) { diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts index 929645433c..423bb6c1c3 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -142,8 +142,11 @@ const fieldsParam = [ 'displayFollowUpLabel', 'displayOrgUnitLabel', 'displayRelationshipLabel', + 'displayRelationshipsLabel', 'displayNoteLabel', + 'displayNotesLabel', 'displayTrackedEntityAttributeLabel', + 'displayTrackedEntityAttributesLabel', 'displayProgramStageLabel', 'displayProgramStagesLabel', 'displayEventLabel', diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts index 676c11b68a..4849b10ab2 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts @@ -147,8 +147,11 @@ type apiProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, + displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, + displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, + displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, diff --git a/src/core_modules/capture-core/storageControllers/types/cache.types.ts b/src/core_modules/capture-core/storageControllers/types/cache.types.ts index 771a55c14b..d903c0ab50 100644 --- a/src/core_modules/capture-core/storageControllers/types/cache.types.ts +++ b/src/core_modules/capture-core/storageControllers/types/cache.types.ts @@ -215,8 +215,11 @@ export type CachedProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, + displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, + displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, + displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, From 18351e92b5ddeb6021da8ea5f7616466c3bf751b Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:09:36 +0000 Subject: [PATCH 002/118] feat: update name assignment in TrackedEntityTypeFactory to use translated name --- i18n/en.pot | 4 ++-- .../factory/TrackedEntityType/TrackedEntityTypeFactory.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9b3d4b0088..24bb1fa3ed 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-07-23T08:24:04.879Z\n" -"PO-Revision-Date: 2026-07-23T08:24:04.880Z\n" +"POT-Creation-Date: 2026-07-23T09:09:38.136Z\n" +"PO-Revision-Date: 2026-07-23T09:09:38.136Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index 3abe925f6e..e5c1359aae 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -84,12 +84,13 @@ export class TrackedEntityTypeFactory { const trackedEntityType = new TrackedEntityType((o) => { o.id = cachedType.id; o.access = cachedType.access; - o.name = this._getTranslation( + const name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; + o.name = name; o.customLabels = extractCustomLabels({ ...cachedType, - displayTrackedEntityTypeLabel: cachedType.displayName, + displayTrackedEntityTypeLabel: name, }); }); From 5a895cdb618890effbed44faef4547940460d6b3 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:36:45 +0000 Subject: [PATCH 003/118] feat: metadata loading support custom plural labels based on server version --- i18n/en.pot | 4 +- src/components/AppLoader/init.ts | 2 +- .../baseLoader/loadMetaData.ts | 3 +- .../metaDataStoreLoaders/context/context.ts | 2 + .../context/context.types.ts | 1 + .../quickStoreOperations/storePrograms.ts | 48 ++++++++++++++----- .../storeTrackedEntityTypes.ts | 15 ++++-- 7 files changed, 54 insertions(+), 21 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 24bb1fa3ed..5557630958 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-07-23T09:09:38.136Z\n" -"PO-Revision-Date: 2026-07-23T09:09:38.136Z\n" +"POT-Creation-Date: 2026-07-23T10:36:47.713Z\n" +"PO-Revision-Date: 2026-07-23T10:36:47.713Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/components/AppLoader/init.ts b/src/components/AppLoader/init.ts index 4418f77ba7..1b6d5465bb 100644 --- a/src/components/AppLoader/init.ts +++ b/src/components/AppLoader/init.ts @@ -121,7 +121,7 @@ async function setLocaleDataAsync(uiLocale: string) { } async function initializeMetaDataAsync(dbLocale: string, onQueryApi: any, minorServerVersion: number) { - await loadMetaData(onQueryApi); + await loadMetaData(onQueryApi, minorServerVersion); await buildMetaDataAsync(dbLocale, minorServerVersion); } diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts index ad544521e6..7e5261b959 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts @@ -3,10 +3,11 @@ import { provideContext } from '../context'; import { loadMetaDataInternal } from './loadMetaDataInternal'; import type { QuerySingleResource } from '../../utils/api'; -export const loadMetaData = async (onQueryApi: QuerySingleResource) => { +export const loadMetaData = async (onQueryApi: QuerySingleResource, minorServerVersion: number) => { await provideContext({ onQueryApi, storageController: getUserMetadataStorageController(), storeNames: USER_METADATA_STORES, + minorServerVersion, }, loadMetaDataInternal); }; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts index fa5346222f..9af346c360 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts @@ -7,12 +7,14 @@ export const provideContext = async ( onQueryApi, storageController, storeNames, + minorServerVersion, }: ContextInput, callback: any) => { context = { onQueryApi, storageController, storeNames, + minorServerVersion, }; await callback(); context = null; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts index 43d07e0e7a..bf7a464819 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts @@ -24,4 +24,5 @@ export type ContextInput = { onQueryApi: QuerySingleResource, storageController: StorageController, storeNames: StoreNames, + minorServerVersion: number, }; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts index 423bb6c1c3..5280eb4240 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -79,6 +79,8 @@ const convert = (() => { }; })(); +const CUSTOM_PLURAL_LABELS_MIN_VERSION = 43; + const programStageDataElementFields = [ 'compulsory', 'displayInReports', @@ -97,7 +99,7 @@ const programTrackedEntityAttributeFields = [ 'allowFutureDate', ].join(','); -const programStageFields = [ +const baseProgramStageFields = [ 'id', 'access', 'autoGenerateEvent', @@ -117,7 +119,6 @@ const programStageFields = [ 'displayDueDateLabel', 'displayProgramStageLabel', 'displayEventLabel', - 'displayEventsLabel', 'formType', 'featureType', 'validationStrategy', @@ -126,9 +127,13 @@ const programStageFields = [ 'dataEntryForm[id,htmlCode]', 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]]', `programStageDataElements[${programStageDataElementFields}]`, -].join(','); +]; + +const pluralProgramStageFields = [ + 'displayEventsLabel', +]; -const fieldsParam = [ +const baseProgramFields = [ 'id', 'displayName', 'displayShortName', @@ -138,19 +143,13 @@ const fieldsParam = [ 'displayIncidentDateLabel', 'displayEnrollmentDateLabel', 'displayEnrollmentLabel', - 'displayEnrollmentsLabel', 'displayFollowUpLabel', 'displayOrgUnitLabel', 'displayRelationshipLabel', - 'displayRelationshipsLabel', 'displayNoteLabel', - 'displayNotesLabel', 'displayTrackedEntityAttributeLabel', - 'displayTrackedEntityAttributesLabel', 'displayProgramStageLabel', - 'displayProgramStagesLabel', 'displayEventLabel', - 'displayEventsLabel', 'minAttributesRequiredToSearch', 'useFirstStageDuringRegistration', 'onlyEnrollOnce', @@ -166,16 +165,39 @@ const fieldsParam = [ 'access[data[read,write]]', 'trackedEntityType[id]', 'categoryCombo[id,displayName,isDefault,categories[id,displayName]]', - `programStages[${programStageFields}]`, 'programSections[id, displayDescription, displayFormName, sortOrder, trackedEntityAttributes]', `programTrackedEntityAttributes[${programTrackedEntityAttributeFields}]`, -].join(','); +]; + +const pluralProgramFields = [ + 'displayEnrollmentsLabel', + 'displayRelationshipsLabel', + 'displayNotesLabel', + 'displayTrackedEntityAttributesLabel', + 'displayProgramStagesLabel', + 'displayEventsLabel', +]; + +const buildFieldsParam = (includePluralLabels: boolean): string => { + const stageFields = includePluralLabels + ? [...baseProgramStageFields, ...pluralProgramStageFields] + : baseProgramStageFields; + const programFields = includePluralLabels + ? [...baseProgramFields, ...pluralProgramFields] + : baseProgramFields; + return [ + ...programFields, + `programStages[${stageFields.join(',')}]`, + ].join(','); +}; export const storePrograms = (programIds: Array) => { + const { minorServerVersion } = getContext(); + const includePluralLabels = minorServerVersion >= CUSTOM_PLURAL_LABELS_MIN_VERSION; const query = { resource: 'programs', params: { - fields: fieldsParam, + fields: buildFieldsParam(includePluralLabels), filter: `id:in:[${programIds.join(',')}]`, pageSize: programIds.length, }, diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts index 1e6f04c161..01d91056c3 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts @@ -26,15 +26,22 @@ const convert = (() => { })); })(); -const fieldsParam = 'id,access,displayName,displayTrackedEntityTypesLabel,minAttributesRequiredToSearch,featureType,' + - 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + - 'translations[property,locale,value]'; +const CUSTOM_PLURAL_LABELS_MIN_VERSION = 43; + +const buildFieldsParam = (includePluralLabels: boolean): string => { + const labels = includePluralLabels ? 'displayName,displayTrackedEntityTypesLabel' : 'displayName'; + return `id,access,${labels},minAttributesRequiredToSearch,featureType,` + + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + + 'translations[property,locale,value]'; +}; export const storeTrackedEntityTypes = (ids: Array) => { + const { minorServerVersion } = getContext(); + const includePluralLabels = minorServerVersion >= CUSTOM_PLURAL_LABELS_MIN_VERSION; const query = { resource: 'trackedEntityTypes', params: { - fields: fieldsParam, + fields: buildFieldsParam(includePluralLabels), filter: `id:in:[${ids.join(',')}]`, pageSize: ids.length, }, From 8cedb6814ee212fa3fd24a54413cbf34beb9ab54 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:12:51 +0000 Subject: [PATCH 004/118] feat: add customTerminologyPlurals feature support --- i18n/en.pot | 4 ++-- src/components/AppLoader/init.ts | 2 +- .../capture-core-utils/featuresSupport/support.ts | 2 ++ .../metaDataStoreLoaders/baseLoader/loadMetaData.ts | 3 +-- .../capture-core/metaDataStoreLoaders/context/context.ts | 2 -- .../metaDataStoreLoaders/context/context.types.ts | 1 - .../programs/quickStoreOperations/storePrograms.ts | 6 ++---- .../quickStoreOperations/storeTrackedEntityTypes.ts | 6 ++---- 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 5557630958..c7a795cd61 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-07-23T10:36:47.713Z\n" -"PO-Revision-Date: 2026-07-23T10:36:47.713Z\n" +"POT-Creation-Date: 2026-07-23T11:12:53.721Z\n" +"PO-Revision-Date: 2026-07-23T11:12:53.722Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/components/AppLoader/init.ts b/src/components/AppLoader/init.ts index 1b6d5465bb..4418f77ba7 100644 --- a/src/components/AppLoader/init.ts +++ b/src/components/AppLoader/init.ts @@ -121,7 +121,7 @@ async function setLocaleDataAsync(uiLocale: string) { } async function initializeMetaDataAsync(dbLocale: string, onQueryApi: any, minorServerVersion: number) { - await loadMetaData(onQueryApi, minorServerVersion); + await loadMetaData(onQueryApi); await buildMetaDataAsync(dbLocale, minorServerVersion); } diff --git a/src/core_modules/capture-core-utils/featuresSupport/support.ts b/src/core_modules/capture-core-utils/featuresSupport/support.ts index 5a0bd6c001..2dbca8e779 100644 --- a/src/core_modules/capture-core-utils/featuresSupport/support.ts +++ b/src/core_modules/capture-core-utils/featuresSupport/support.ts @@ -18,6 +18,7 @@ export const FEATURES = Object.freeze({ orgUnitReplaceOuQueryParam: 'orgUnitReplaceOuQueryParam', enrollmentStatusReplaceProgramStatusQueryParam: 'enrollmentStatusReplaceProgramStatusQueryParam', emptyValueFilter: 'emptyValueFilter', + customTerminologyPlurals: 'customTerminologyPlurals', }); const MINOR_VERSION_SUPPORT = Object.freeze({ @@ -40,6 +41,7 @@ const MINOR_VERSION_SUPPORT = Object.freeze({ [FEATURES.orgUnitReplaceOuQueryParam]: 42, [FEATURES.enrollmentStatusReplaceProgramStatusQueryParam]: 42, [FEATURES.emptyValueFilter]: 42, + [FEATURES.customTerminologyPlurals]: 43, }); export const hasAPISupportForFeature = (minorVersion: string | number, featureName: string) => diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts index 7e5261b959..ad544521e6 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/baseLoader/loadMetaData.ts @@ -3,11 +3,10 @@ import { provideContext } from '../context'; import { loadMetaDataInternal } from './loadMetaDataInternal'; import type { QuerySingleResource } from '../../utils/api'; -export const loadMetaData = async (onQueryApi: QuerySingleResource, minorServerVersion: number) => { +export const loadMetaData = async (onQueryApi: QuerySingleResource) => { await provideContext({ onQueryApi, storageController: getUserMetadataStorageController(), storeNames: USER_METADATA_STORES, - minorServerVersion, }, loadMetaDataInternal); }; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts index 9af346c360..fa5346222f 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.ts @@ -7,14 +7,12 @@ export const provideContext = async ( onQueryApi, storageController, storeNames, - minorServerVersion, }: ContextInput, callback: any) => { context = { onQueryApi, storageController, storeNames, - minorServerVersion, }; await callback(); context = null; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts index bf7a464819..43d07e0e7a 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/context/context.types.ts @@ -24,5 +24,4 @@ export type ContextInput = { onQueryApi: QuerySingleResource, storageController: StorageController, storeNames: StoreNames, - minorServerVersion: number, }; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts index 5280eb4240..48a9dddec6 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -1,3 +1,4 @@ +import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; import { quickStore } from '../../IOUtils'; import { getContext } from '../../context'; import type { CachedProgramStageDataElement } from '../../../storageControllers'; @@ -79,8 +80,6 @@ const convert = (() => { }; })(); -const CUSTOM_PLURAL_LABELS_MIN_VERSION = 43; - const programStageDataElementFields = [ 'compulsory', 'displayInReports', @@ -192,8 +191,7 @@ const buildFieldsParam = (includePluralLabels: boolean): string => { }; export const storePrograms = (programIds: Array) => { - const { minorServerVersion } = getContext(); - const includePluralLabels = minorServerVersion >= CUSTOM_PLURAL_LABELS_MIN_VERSION; + const includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); const query = { resource: 'programs', params: { diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts index 01d91056c3..bb12dba43c 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts @@ -1,3 +1,4 @@ +import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; import { quickStore } from '../../IOUtils'; import { getContext } from '../../context'; @@ -26,8 +27,6 @@ const convert = (() => { })); })(); -const CUSTOM_PLURAL_LABELS_MIN_VERSION = 43; - const buildFieldsParam = (includePluralLabels: boolean): string => { const labels = includePluralLabels ? 'displayName,displayTrackedEntityTypesLabel' : 'displayName'; return `id,access,${labels},minAttributesRequiredToSearch,featureType,` + @@ -36,8 +35,7 @@ const buildFieldsParam = (includePluralLabels: boolean): string => { }; export const storeTrackedEntityTypes = (ids: Array) => { - const { minorServerVersion } = getContext(); - const includePluralLabels = minorServerVersion >= CUSTOM_PLURAL_LABELS_MIN_VERSION; + const includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); const query = { resource: 'trackedEntityTypes', params: { From c4799537879dc4d98bfe3ae02e0b7a088a0a790a Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:56:46 +0000 Subject: [PATCH 005/118] fix: separate base and plural fields for improved readability --- i18n/en.pot | 4 +-- .../WidgetEnrollment/hooks/useProgram.ts | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 00e43fbda8..b19a9f94f4 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-06T09:39:23.801Z\n" -"PO-Revision-Date: 2026-08-06T09:39:23.801Z\n" +"POT-Creation-Date: 2026-08-06T09:56:48.432Z\n" +"PO-Revision-Date: 2026-08-06T09:56:48.432Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index 048ac5018c..1e2d99395a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -1,11 +1,27 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; +import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; type ProgramData = { featureType: string; [key: string]: any; }; +const baseFields = [ + 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,' + + 'displayEnrollmentLabel,displayFollowUpLabel,displayOrgUnitLabel,' + + 'displayRelationshipLabel,displayNoteLabel,displayTrackedEntityAttributeLabel,' + + 'displayProgramStageLabel,displayEventLabel,' + + 'trackedEntityType[displayName,access],' + + 'programStages[autoGenerateEvent,name,access,id],' + + 'access,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture', +]; + +const pluralFields = [ + 'displayEnrollmentsLabel,displayRelationshipsLabel,displayNotesLabel,' + + 'displayTrackedEntityAttributesLabel,displayProgramStagesLabel,displayEventsLabel', +]; + export const useProgram = (programId: string) => { const { error, loading, data } = useDataQuery( useMemo( @@ -13,15 +29,9 @@ export const useProgram = (programId: string) => { program: { resource: `programs/${programId}`, params: { - fields: [ - 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,' + - 'displayEnrollmentLabel,displayEnrollmentsLabel,displayFollowUpLabel,displayOrgUnitLabel,' + - 'displayRelationshipLabel,displayNoteLabel,displayTrackedEntityAttributeLabel,' + - 'displayProgramStageLabel,displayProgramStagesLabel,displayEventLabel,displayEventsLabel,' + - 'trackedEntityType[displayName,access],' + - 'programStages[autoGenerateEvent,name,access,id],' + - 'access,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture', - ], + fields: featureAvailable(FEATURES.customTerminologyPlurals) + ? [...baseFields, ...pluralFields] + : baseFields, }, }, }), From e5b4d74be08d88ba699fca48a90904c786796132 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 06:49:33 +0000 Subject: [PATCH 006/118] feat: temp change helper function and stop capitalize --- i18n/en.pot | 4 +- .../metaData/helpers/customLabels/index.ts | 8 ++-- .../metaData/helpers/customLabels/useLabel.ts | 45 ------------------- .../capture-core/metaData/helpers/index.ts | 10 ++--- .../capture-core/metaData/index.ts | 10 ++--- 5 files changed, 11 insertions(+), 66 deletions(-) delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts diff --git a/i18n/en.pot b/i18n/en.pot index b19a9f94f4..9db2b4dd84 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-06T09:56:48.432Z\n" -"PO-Revision-Date: 2026-08-06T09:56:48.432Z\n" +"POT-Creation-Date: 2026-08-07T06:49:34.201Z\n" +"PO-Revision-Date: 2026-08-07T06:49:34.201Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index 49b34132fe..a698f8740c 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -1,10 +1,8 @@ export { CUSTOM_LABEL_FIELDS, - resolveLabel, + resolveCustomLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; -export { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } from './useLabel'; +export { applyCustomTerminology } from './applyCustomTerminology'; +export { bootstrapCustomTerminology } from './bootstrapCustomTerminology'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts deleted file mode 100644 index c733c2e662..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; -import { resolveLabel } from './customLabels'; -import type { CustomLabelKey, LabelOptions } from './customLabels'; - -type ProgramOptions = LabelOptions & { programId?: string }; -type StageOptions = LabelOptions & { programId?: string, stageId?: string }; -type TrackedEntityTypeOptions = LabelOptions & { tetId?: string }; - -export const useProgramLabel = (key: CustomLabelKey, { programId, plural }: ProgramOptions = {}): string | undefined => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const id = programId ?? currentProgramId; - return useMemo( - () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural }), - [id, key, plural], - ); -}; - -export const useStageLabel = ( - key: CustomLabelKey, - { programId, stageId, plural }: StageOptions = {}, -): string | undefined => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const currentStageId = useSelector(({ currentSelections }: any) => currentSelections.stageId); - const pId = programId ?? currentProgramId; - const sId = stageId ?? currentStageId; - return useMemo(() => { - const program = pId ? programCollection.get(pId) : undefined; - const stage = program && sId ? program.getStage(sId) : undefined; - return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); - }, [pId, sId, key, plural]); -}; - -export const useTrackedEntityTypeLabel = ( - key: CustomLabelKey, - { tetId, plural }: TrackedEntityTypeOptions = {}, -): string | undefined => { - const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); - const id = tetId ?? currentTetId; - return useMemo( - () => resolveLabel(id ? trackedEntityTypesCollection.get(id)?.customLabels : undefined, key, { plural }), - [id, key, plural], - ); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index 627adbd3b3..e5a22c7396 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -19,13 +19,9 @@ export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { CUSTOM_LABEL_FIELDS, - resolveLabel, + resolveCustomLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, - useProgramLabel, - useStageLabel, - useTrackedEntityTypeLabel, + applyCustomTerminology, + bootstrapCustomTerminology, } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 00e7aca7aa..06898f4539 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -41,13 +41,9 @@ export { getProgramAndStageForEventProgram, getEventProgramEventAccess, CUSTOM_LABEL_FIELDS, - resolveLabel, + resolveCustomLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, - useProgramLabel, - useStageLabel, - useTrackedEntityTypeLabel, + applyCustomTerminology, + bootstrapCustomTerminology, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; From 302cc2bc52a6fdb83ac410453fd1ce1bb62dcb1e Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:15:44 +0000 Subject: [PATCH 007/118] feat: implement custom terminology handling with bootstrap --- i18n/en.pot | 4 +- .../customLabels/applyCustomTerminology.ts | 99 +++++++++++++++++++ .../bootstrapCustomTerminology.ts | 30 ++++++ .../helpers/customLabels/customLabels.ts | 99 ++++++++++++------- .../metaData/helpers/customLabels/index.ts | 1 + .../customLabels/resolveTerminologyContext.ts | 58 +++++++++++ src/declarations.d.ts | 3 + src/store/getStore.ts | 3 + 8 files changed, 257 insertions(+), 40 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts diff --git a/i18n/en.pot b/i18n/en.pot index 9db2b4dd84..7cb5db531b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T06:49:34.201Z\n" -"PO-Revision-Date: 2026-08-07T06:49:34.201Z\n" +"POT-Creation-Date: 2026-08-07T08:15:45.211Z\n" +"PO-Revision-Date: 2026-08-07T08:15:45.213Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts new file mode 100644 index 0000000000..1608da0285 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -0,0 +1,99 @@ +import i18n from '@dhis2/d2-i18n'; +import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; +import { CUSTOM_LABEL_FIELDS, resolveCustomLabel } from './customLabels'; +import type { CustomLabelKey, CustomLabels, CustomLabelField } from './customLabels'; + +export type TerminologyContext = { + programId?: string, + stageId?: string, + trackedEntityTypeId?: string, +}; + +type TermEntry = { + key: CustomLabelKey, + plural: boolean, + english: string, +}; + +// Derive the flat list of match candidates from CUSTOM_LABEL_FIELDS (single source +// of truth). Includes each form's English word plus any aliases (e.g. "stage" for +// programStage.singular). Sorted longest-first so multi-word forms are tried before +// their sub-strings — the combined regex's alternation then respects that order. +const TERM_ENTRIES: ReadonlyArray = ( + Object.entries(CUSTOM_LABEL_FIELDS) as ReadonlyArray<[CustomLabelKey, CustomLabelField]> +).flatMap(([key, forms]) => { + const out: TermEntry[] = []; + const addForm = (form: { english: string, aliases?: ReadonlyArray }, plural: boolean) => { + out.push({ key, plural, english: form.english }); + (form.aliases ?? []).forEach(alias => out.push({ key, plural, english: alias })); + }; + if (forms.plural) addForm(forms.plural, true); + addForm(forms.singular, false); + return out; +}).sort((a, b) => b.english.length - a.english.length); + +const escapeRegExp = (input: string): string => input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +const COMBINED_PATTERN = new RegExp( + `\\b(${TERM_ENTRIES.map(entry => escapeRegExp(entry.english)).join('|')})\\b`, + 'gi', +); + +const findEntry = (match: string): TermEntry | undefined => { + const lower = match.toLowerCase(); + return TERM_ENTRIES.find(entry => entry.english === lower); +}; + +const preserveCase = (match: string, replacement: string, locale: string): string => { + if (match.length > 1 && match === match.toLocaleUpperCase(locale)) { + return replacement.toLocaleUpperCase(locale); + } + if (match[0] === match[0].toLocaleUpperCase(locale)) { + return replacement.charAt(0).toLocaleUpperCase(locale) + replacement.slice(1); + } + return replacement; +}; + +const getLabelSources = ({ + programId, + stageId, + trackedEntityTypeId, +}: TerminologyContext): Array => { + const program = programId ? programCollection.get(programId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + const tet = trackedEntityTypeId ? trackedEntityTypesCollection.get(trackedEntityTypeId) : undefined; + // Precedence: stage overrides program overrides TET. + return [stage?.customLabels, program?.customLabels, tet?.customLabels]; +}; + +/** + * Substitute DHIS2 terminology (enrollment, event, note, relationship, ...) in a + * translated string with per-program custom labels when configured. Case in the + * source string is preserved on the substituted term. Locale-aware via `i18n.language`. + * + * @example + * applyCustomTerminology(i18n.t('Write a note about this enrollment'), { programId }) + */ +export const applyCustomTerminology = ( + translatedText: string, + context: TerminologyContext = {}, +): string => { + // Defensive: t() can return non-string values (arrays/objects when + // returnObjects: true, undefined for missing keys with certain configs). + // Only strings go through the substitution pipeline; everything else is + // returned as-is so callers see the original i18next output unchanged. + if (typeof translatedText !== 'string' || !translatedText) return translatedText; + const { programId, stageId, trackedEntityTypeId } = context; + if (!programId && !stageId && !trackedEntityTypeId) return translatedText; + + const sources = getLabelSources(context); + const locale = i18n.language || 'en'; + + return translatedText.replace(COMBINED_PATTERN, (match) => { + const entry = findEntry(match); + if (!entry) return match; + const custom = resolveCustomLabel(sources, entry.key, { plural: entry.plural }); + if (!custom) return match; + return preserveCase(match, custom, locale); + }); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts new file mode 100644 index 0000000000..12eba2b70c --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts @@ -0,0 +1,30 @@ +import i18n from '@dhis2/d2-i18n'; +import { applyCustomTerminology } from './applyCustomTerminology'; +import { resolveTerminologyContext } from './resolveTerminologyContext'; + +type StoreLike = { getState: () => unknown }; + +let bootstrapped = false; + +/** + * Wraps i18n.t so every call passes its result through applyCustomTerminology, + * substituting DHIS2 terms with the current program's custom labels. Program / + * stage / tracked entity type ids are resolved per call via + * resolveTerminologyContext (URL first, then Redux domain state). + * + * Callers can opt out per call with i18n.t(key, { postProcess: false }) — for + * strings that mention DHIS2 terms in the everyday sense. + * + * Call once at app bootstrap. + */ +export const bootstrapCustomTerminology = (store: StoreLike) => { + if (bootstrapped) return; + bootstrapped = true; + + const originalT = i18n.t.bind(i18n); + i18n.t = (key: string, options?: any) => { + const translated = originalT(key, options); + if (options?.postProcess === false) return translated; + return applyCustomTerminology(translated, resolveTerminologyContext(store)); + }; +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 0366c64f64..56670d7c35 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,20 +1,64 @@ -import { capitalizeFirstLetter } from 'capture-core-utils/string'; +// Owns the shape and lookup of program/stage/tracked-entity-type custom labels: +// - CUSTOM_LABEL_FIELDS is the single source of truth: for each DHIS2 term it +// names the API field to read (per singular/plural), the English word to +// look for in translated strings, and any aliases. +// - extractCustomLabels reads them off the API cache when domain objects are +// built (ProgramFactory / ProgramStageFactory / TrackedEntityTypeFactory). +// - resolveCustomLabel picks a label from those sources at substitution time +// (called by the postProcessor in applyCustomTerminology). +// Previously also exported resolveLabel/getProgramLabel/useProgramLabel which +// forced capitalization at every call site; that pattern is gone since the +// postProcessor now handles case at the point of substitution. -type CustomLabelField = { - singular?: string, - plural?: string, +export type CustomLabelForm = { + field: string, + english: string, + aliases?: ReadonlyArray, +}; + +export type CustomLabelField = { + singular: CustomLabelForm, + plural?: CustomLabelForm, }; export const CUSTOM_LABEL_FIELDS = { - enrollment: { singular: 'displayEnrollmentLabel', plural: 'displayEnrollmentsLabel' }, - followUp: { singular: 'displayFollowUpLabel' }, - orgUnit: { singular: 'displayOrgUnitLabel' }, - note: { singular: 'displayNoteLabel', plural: 'displayNotesLabel' }, - relationship: { singular: 'displayRelationshipLabel', plural: 'displayRelationshipsLabel' }, - attribute: { singular: 'displayTrackedEntityAttributeLabel', plural: 'displayTrackedEntityAttributesLabel' }, - programStage: { singular: 'displayProgramStageLabel', plural: 'displayProgramStagesLabel' }, - event: { singular: 'displayEventLabel', plural: 'displayEventsLabel' }, - trackedEntityType: { singular: 'displayTrackedEntityTypeLabel', plural: 'displayTrackedEntityTypesLabel' }, + enrollment: { + singular: { field: 'displayEnrollmentLabel', english: 'enrollment' }, + plural: { field: 'displayEnrollmentsLabel', english: 'enrollments' }, + }, + event: { + singular: { field: 'displayEventLabel', english: 'event' }, + plural: { field: 'displayEventsLabel', english: 'events' }, + }, + note: { + singular: { field: 'displayNoteLabel', english: 'note' }, + plural: { field: 'displayNotesLabel', english: 'notes' }, + }, + relationship: { + singular: { field: 'displayRelationshipLabel', english: 'relationship' }, + plural: { field: 'displayRelationshipsLabel', english: 'relationships' }, + }, + attribute: { + singular: { field: 'displayTrackedEntityAttributeLabel', english: 'attribute' }, + plural: { field: 'displayTrackedEntityAttributesLabel', english: 'attributes' }, + }, + programStage: { + singular: { field: 'displayProgramStageLabel', english: 'program stage', aliases: ['stage'] }, + plural: { field: 'displayProgramStagesLabel', english: 'program stages', aliases: ['stages'] }, + }, + // API only exposes a singular custom label for orgUnit; we reuse it for the + // plural form so "organisation units" resolves to the same admin-set string. + orgUnit: { + singular: { field: 'displayOrgUnitLabel', english: 'organisation unit' }, + plural: { field: 'displayOrgUnitLabel', english: 'organisation units' }, + }, + trackedEntityType: { + singular: { field: 'displayTrackedEntityTypeLabel', english: 'tracked entity' }, + plural: { field: 'displayTrackedEntityTypesLabel', english: 'tracked entities' }, + }, + followUp: { + singular: { field: 'displayFollowUpLabel', english: 'follow-up' }, + }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; @@ -24,7 +68,7 @@ export type LabelOptions = { plural?: boolean }; const ALL_FIELDS: ReadonlyArray = Array.from( new Set( Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.singular, term.plural]) + .flatMap((term: CustomLabelField) => [term.singular.field, term.plural?.field]) .filter((field): field is string => Boolean(field)), ), ); @@ -42,34 +86,13 @@ export const extractCustomLabels = (cached: Record): CustomLabe type LabelSource = CustomLabels | undefined | null; -export const resolveLabel = ( +export const resolveCustomLabel = ( sources: LabelSource | Array, key: CustomLabelKey, { plural = false }: LabelOptions = {}, ): string | undefined => { const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; const list = Array.isArray(sources) ? sources : [sources]; - const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); - - const field = plural && term.plural ? term.plural : term.singular; - const value = pick(field); - return value ? capitalizeFirstLetter(value) : value; + const form = plural && term.plural ? term.plural : term.singular; + return list.find(source => source?.[form.field])?.[form.field]; }; - -type WithLabels = { customLabels?: CustomLabels } | undefined | null; - -export const getProgramLabel = (program: WithLabels, key: CustomLabelKey, options?: LabelOptions): string | undefined => - resolveLabel(program?.customLabels, key, options); - -export const getStageLabel = ( - stage: WithLabels, - program: WithLabels, - key: CustomLabelKey, - options?: LabelOptions, -): string | undefined => resolveLabel([stage?.customLabels, program?.customLabels], key, options); - -export const getTrackedEntityTypeLabel = ( - trackedEntityType: WithLabels, - key: CustomLabelKey, - options?: LabelOptions, -): string | undefined => resolveLabel(trackedEntityType?.customLabels, key, options); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index a698f8740c..06a2a4cd5d 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -5,4 +5,5 @@ export { } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; export { applyCustomTerminology } from './applyCustomTerminology'; +export type { TerminologyContext } from './applyCustomTerminology'; export { bootstrapCustomTerminology } from './bootstrapCustomTerminology'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts new file mode 100644 index 0000000000..a882d38430 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts @@ -0,0 +1,58 @@ +import { getLocationQuery } from '../../../utils/routing'; +import type { TerminologyContext } from './applyCustomTerminology'; + +type StoreLike = { getState: () => unknown }; + +type DomainState = { + viewEventPage?: { + loadedValues?: { + eventContainer?: { event?: { program?: string, programStage?: string } }, + }, + }, + enrollmentDomain?: { + enrollment?: { program?: string }, + }, +}; + +/** + * Layered resolution of the "current view's program" for terminology substitution. + * 1. URL query — most program-scoped pages carry programId directly (enrollment + * dashboard, working lists, new enrollment, etc.). + * 2. Redux domain state — pages that carry only entity ids (event edit's eventId, + * viewEvent's viewEventId, TEI dashboard's teiId) resolve program via the + * loaded entity. + * 3. Nothing — English fallback. Never touches state.currentSelections, which is + * the top-nav scope filter and can diverge from the entity actually on screen. + */ +export const resolveTerminologyContext = (store: StoreLike): TerminologyContext => { + const query = getLocationQuery(); + + // Layer 1: URL + if (query.programId) { + return { + programId: query.programId, + stageId: query.stageId ?? query.programStageId, + trackedEntityTypeId: query.trackedEntityTypeId, + }; + } + + // Layer 2: Redux domain state + const state = (store.getState() ?? {}) as DomainState; + + if (query.eventId || query.viewEventId) { + const event = state.viewEventPage?.loadedValues?.eventContainer?.event; + if (event?.program) { + return { programId: event.program, stageId: event.programStage }; + } + } + + if (query.enrollmentId || query.teiId) { + const enrollment = state.enrollmentDomain?.enrollment; + if (enrollment?.program) { + return { programId: enrollment.program }; + } + } + + // Layer 3: no context — postProcessor will leave the string untouched. + return {}; +}; diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 1cc6a18172..b3de0cd798 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -13,6 +13,9 @@ declare module 'src/core_modules/*'; declare module '@dhis2/d2-i18n' { const i18n: { t: (key: string, options?: any) => any; + // Added for applyCustomTerminology, which passes the active locale to + // toLocaleUpperCase/toLocaleLowerCase for correct casing (e.g. Turkish i/İ). + language: string; // Add other methods as needed }; export default i18n; diff --git a/src/store/getStore.ts b/src/store/getStore.ts index 699b66dbe6..ac3596982a 100644 --- a/src/store/getStore.ts +++ b/src/store/getStore.ts @@ -9,6 +9,7 @@ import { environments } from 'capture-core/constants/environments'; import { createOffline } from '@redux-offline/redux-offline'; import offlineConfig from '@redux-offline/redux-offline/lib/defaults'; import { getEffectReconciler, shouldDiscard, queueConfig } from 'capture-core/trackerOffline'; +import { bootstrapCustomTerminology } from 'capture-core/metaData/helpers/customLabels'; import { getPersistOptions } from './persist/persistOptionsGetter'; import { reducerDescriptions } from '../reducers/descriptions/trackerCapture.reducerDescriptions'; import { epics } from '../epics/trackerCapture.epics'; @@ -55,5 +56,7 @@ export async function getStore( epicMiddleware.run(epics); + bootstrapCustomTerminology(store); + return store; } From 823f4e11be65fcc4febbf6ac1f4a85b7f011ad10 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:45:51 +0000 Subject: [PATCH 008/118] fix: revert changes belonging to child branch --- i18n/en.pot | 4 ++-- .../components/WidgetEnrollment/hooks/useProgram.ts | 3 +-- .../helpers/customLabels/applyCustomTerminology.ts | 2 +- .../metaData/helpers/customLabels/customLabels.ts | 9 +++------ .../TrackedEntityType/TrackedEntityTypeFactory.ts | 8 ++------ .../programs/quickStoreOperations/storePrograms.ts | 3 --- .../quickStoreOperations/types/apiPrograms.types.ts | 3 --- .../capture-core/storageControllers/types/cache.types.ts | 3 --- 8 files changed, 9 insertions(+), 26 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7cb5db531b..602e2de115 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T08:15:45.211Z\n" -"PO-Revision-Date: 2026-08-07T08:15:45.213Z\n" +"POT-Creation-Date: 2026-08-07T08:45:53.252Z\n" +"PO-Revision-Date: 2026-08-07T08:45:53.252Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index 1e2d99395a..b76d2e87ce 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -18,8 +18,7 @@ const baseFields = [ ]; const pluralFields = [ - 'displayEnrollmentsLabel,displayRelationshipsLabel,displayNotesLabel,' + - 'displayTrackedEntityAttributesLabel,displayProgramStagesLabel,displayEventsLabel', + 'displayEnrollmentsLabel,displayProgramStagesLabel,displayEventsLabel', ]; export const useProgram = (programId: string) => { diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index 1608da0285..b5f95242e4 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -28,7 +28,7 @@ const TERM_ENTRIES: ReadonlyArray = ( (form.aliases ?? []).forEach(alias => out.push({ key, plural, english: alias })); }; if (forms.plural) addForm(forms.plural, true); - addForm(forms.singular, false); + if (forms.singular) addForm(forms.singular, false); return out; }).sort((a, b) => b.english.length - a.english.length); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 56670d7c35..8c15d0ad43 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -17,7 +17,7 @@ export type CustomLabelForm = { }; export type CustomLabelField = { - singular: CustomLabelForm, + singular?: CustomLabelForm, plural?: CustomLabelForm, }; @@ -32,15 +32,12 @@ export const CUSTOM_LABEL_FIELDS = { }, note: { singular: { field: 'displayNoteLabel', english: 'note' }, - plural: { field: 'displayNotesLabel', english: 'notes' }, }, relationship: { singular: { field: 'displayRelationshipLabel', english: 'relationship' }, - plural: { field: 'displayRelationshipsLabel', english: 'relationships' }, }, attribute: { singular: { field: 'displayTrackedEntityAttributeLabel', english: 'attribute' }, - plural: { field: 'displayTrackedEntityAttributesLabel', english: 'attributes' }, }, programStage: { singular: { field: 'displayProgramStageLabel', english: 'program stage', aliases: ['stage'] }, @@ -53,7 +50,6 @@ export const CUSTOM_LABEL_FIELDS = { plural: { field: 'displayOrgUnitLabel', english: 'organisation units' }, }, trackedEntityType: { - singular: { field: 'displayTrackedEntityTypeLabel', english: 'tracked entity' }, plural: { field: 'displayTrackedEntityTypesLabel', english: 'tracked entities' }, }, followUp: { @@ -68,7 +64,7 @@ export type LabelOptions = { plural?: boolean }; const ALL_FIELDS: ReadonlyArray = Array.from( new Set( Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.singular.field, term.plural?.field]) + .flatMap((term: CustomLabelField) => [term.singular?.field, term.plural?.field]) .filter((field): field is string => Boolean(field)), ), ); @@ -94,5 +90,6 @@ export const resolveCustomLabel = ( const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; const list = Array.isArray(sources) ? sources : [sources]; const form = plural && term.plural ? term.plural : term.singular; + if (!form) return undefined; return list.find(source => source?.[form.field])?.[form.field]; }; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index e5c1359aae..7e5e9d5a91 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -84,14 +84,10 @@ export class TrackedEntityTypeFactory { const trackedEntityType = new TrackedEntityType((o) => { o.id = cachedType.id; o.access = cachedType.access; - const name = this._getTranslation( + o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.name = name; - o.customLabels = extractCustomLabels({ - ...cachedType, - displayTrackedEntityTypeLabel: name, - }); + o.customLabels = extractCustomLabels(cachedType); }); if (cachedType.trackedEntityTypeAttributes) { diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts index 48a9dddec6..7d972501b6 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -170,9 +170,6 @@ const baseProgramFields = [ const pluralProgramFields = [ 'displayEnrollmentsLabel', - 'displayRelationshipsLabel', - 'displayNotesLabel', - 'displayTrackedEntityAttributesLabel', 'displayProgramStagesLabel', 'displayEventsLabel', ]; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts index 4849b10ab2..676c11b68a 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts @@ -147,11 +147,8 @@ type apiProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, - displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, - displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, - displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, diff --git a/src/core_modules/capture-core/storageControllers/types/cache.types.ts b/src/core_modules/capture-core/storageControllers/types/cache.types.ts index d903c0ab50..771a55c14b 100644 --- a/src/core_modules/capture-core/storageControllers/types/cache.types.ts +++ b/src/core_modules/capture-core/storageControllers/types/cache.types.ts @@ -215,11 +215,8 @@ export type CachedProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, - displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, - displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, - displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, From 9f8ccb6384f4195e45792c78f8ae850e5676a48b Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:50:13 +0000 Subject: [PATCH 009/118] Revert "fix: revert changes belonging to child branch" This reverts commit 823f4e11be65fcc4febbf6ac1f4a85b7f011ad10. --- i18n/en.pot | 4 ++-- .../components/WidgetEnrollment/hooks/useProgram.ts | 3 ++- .../helpers/customLabels/applyCustomTerminology.ts | 2 +- .../metaData/helpers/customLabels/customLabels.ts | 9 ++++++--- .../TrackedEntityType/TrackedEntityTypeFactory.ts | 8 ++++++-- .../programs/quickStoreOperations/storePrograms.ts | 3 +++ .../quickStoreOperations/types/apiPrograms.types.ts | 3 +++ .../capture-core/storageControllers/types/cache.types.ts | 3 +++ 8 files changed, 26 insertions(+), 9 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 602e2de115..7cb5db531b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T08:45:53.252Z\n" -"PO-Revision-Date: 2026-08-07T08:45:53.252Z\n" +"POT-Creation-Date: 2026-08-07T08:15:45.211Z\n" +"PO-Revision-Date: 2026-08-07T08:15:45.213Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index b76d2e87ce..1e2d99395a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -18,7 +18,8 @@ const baseFields = [ ]; const pluralFields = [ - 'displayEnrollmentsLabel,displayProgramStagesLabel,displayEventsLabel', + 'displayEnrollmentsLabel,displayRelationshipsLabel,displayNotesLabel,' + + 'displayTrackedEntityAttributesLabel,displayProgramStagesLabel,displayEventsLabel', ]; export const useProgram = (programId: string) => { diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index b5f95242e4..1608da0285 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -28,7 +28,7 @@ const TERM_ENTRIES: ReadonlyArray = ( (form.aliases ?? []).forEach(alias => out.push({ key, plural, english: alias })); }; if (forms.plural) addForm(forms.plural, true); - if (forms.singular) addForm(forms.singular, false); + addForm(forms.singular, false); return out; }).sort((a, b) => b.english.length - a.english.length); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 8c15d0ad43..56670d7c35 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -17,7 +17,7 @@ export type CustomLabelForm = { }; export type CustomLabelField = { - singular?: CustomLabelForm, + singular: CustomLabelForm, plural?: CustomLabelForm, }; @@ -32,12 +32,15 @@ export const CUSTOM_LABEL_FIELDS = { }, note: { singular: { field: 'displayNoteLabel', english: 'note' }, + plural: { field: 'displayNotesLabel', english: 'notes' }, }, relationship: { singular: { field: 'displayRelationshipLabel', english: 'relationship' }, + plural: { field: 'displayRelationshipsLabel', english: 'relationships' }, }, attribute: { singular: { field: 'displayTrackedEntityAttributeLabel', english: 'attribute' }, + plural: { field: 'displayTrackedEntityAttributesLabel', english: 'attributes' }, }, programStage: { singular: { field: 'displayProgramStageLabel', english: 'program stage', aliases: ['stage'] }, @@ -50,6 +53,7 @@ export const CUSTOM_LABEL_FIELDS = { plural: { field: 'displayOrgUnitLabel', english: 'organisation units' }, }, trackedEntityType: { + singular: { field: 'displayTrackedEntityTypeLabel', english: 'tracked entity' }, plural: { field: 'displayTrackedEntityTypesLabel', english: 'tracked entities' }, }, followUp: { @@ -64,7 +68,7 @@ export type LabelOptions = { plural?: boolean }; const ALL_FIELDS: ReadonlyArray = Array.from( new Set( Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.singular?.field, term.plural?.field]) + .flatMap((term: CustomLabelField) => [term.singular.field, term.plural?.field]) .filter((field): field is string => Boolean(field)), ), ); @@ -90,6 +94,5 @@ export const resolveCustomLabel = ( const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; const list = Array.isArray(sources) ? sources : [sources]; const form = plural && term.plural ? term.plural : term.singular; - if (!form) return undefined; return list.find(source => source?.[form.field])?.[form.field]; }; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index 7e5e9d5a91..e5c1359aae 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -84,10 +84,14 @@ export class TrackedEntityTypeFactory { const trackedEntityType = new TrackedEntityType((o) => { o.id = cachedType.id; o.access = cachedType.access; - o.name = this._getTranslation( + const name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.customLabels = extractCustomLabels(cachedType); + o.name = name; + o.customLabels = extractCustomLabels({ + ...cachedType, + displayTrackedEntityTypeLabel: name, + }); }); if (cachedType.trackedEntityTypeAttributes) { diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts index 7d972501b6..48a9dddec6 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -170,6 +170,9 @@ const baseProgramFields = [ const pluralProgramFields = [ 'displayEnrollmentsLabel', + 'displayRelationshipsLabel', + 'displayNotesLabel', + 'displayTrackedEntityAttributesLabel', 'displayProgramStagesLabel', 'displayEventsLabel', ]; diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts index 676c11b68a..4849b10ab2 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts @@ -147,8 +147,11 @@ type apiProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, + displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, + displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, + displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, diff --git a/src/core_modules/capture-core/storageControllers/types/cache.types.ts b/src/core_modules/capture-core/storageControllers/types/cache.types.ts index 771a55c14b..d903c0ab50 100644 --- a/src/core_modules/capture-core/storageControllers/types/cache.types.ts +++ b/src/core_modules/capture-core/storageControllers/types/cache.types.ts @@ -215,8 +215,11 @@ export type CachedProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, + displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, + displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, + displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, From e81170c1c978faf72736adf17bc1be2670f04445 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:55:04 +0000 Subject: [PATCH 010/118] feat: temp --- i18n/en.pot | 4 ++-- .../components/WidgetEnrollment/hooks/useProgram.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7cb5db531b..01811296cb 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T08:15:45.211Z\n" -"PO-Revision-Date: 2026-08-07T08:15:45.213Z\n" +"POT-Creation-Date: 2026-08-07T08:55:05.324Z\n" +"PO-Revision-Date: 2026-08-07T08:55:05.324Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index 1e2d99395a..06e74ae49d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -7,6 +7,7 @@ type ProgramData = { [key: string]: any; }; +// comment const baseFields = [ 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,' + 'displayEnrollmentLabel,displayFollowUpLabel,displayOrgUnitLabel,' + From f29cb712108659652763d54e3dd1acc7505616f0 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:56:56 +0200 Subject: [PATCH 011/118] fix: clean up Removed commented line from useProgram.ts. --- .../capture-core/components/WidgetEnrollment/hooks/useProgram.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index 06e74ae49d..1e2d99395a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -7,7 +7,6 @@ type ProgramData = { [key: string]: any; }; -// comment const baseFields = [ 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,' + 'displayEnrollmentLabel,displayFollowUpLabel,displayOrgUnitLabel,' + From 764e73bf66b16614b0ecc116a9acf34b493ee5f4 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:08:24 +0000 Subject: [PATCH 012/118] fix: sonar qube --- i18n/en.pot | 4 ++-- .../helpers/customLabels/applyCustomTerminology.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 602e2de115..d371813061 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T08:45:53.252Z\n" -"PO-Revision-Date: 2026-08-07T08:45:53.252Z\n" +"POT-Creation-Date: 2026-08-07T09:08:26.197Z\n" +"PO-Revision-Date: 2026-08-07T09:08:26.197Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index b5f95242e4..3d6cd75751 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -32,10 +32,10 @@ const TERM_ENTRIES: ReadonlyArray = ( return out; }).sort((a, b) => b.english.length - a.english.length); -const escapeRegExp = (input: string): string => input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const escapeRegExp = (input: string): string => input.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); const COMBINED_PATTERN = new RegExp( - `\\b(${TERM_ENTRIES.map(entry => escapeRegExp(entry.english)).join('|')})\\b`, + String.raw`\b(${TERM_ENTRIES.map(entry => escapeRegExp(entry.english)).join('|')})\b`, 'gi', ); @@ -48,7 +48,8 @@ const preserveCase = (match: string, replacement: string, locale: string): strin if (match.length > 1 && match === match.toLocaleUpperCase(locale)) { return replacement.toLocaleUpperCase(locale); } - if (match[0] === match[0].toLocaleUpperCase(locale)) { + const firstUpper = match.charAt(0).toLocaleUpperCase(locale); + if (match.startsWith(firstUpper)) { return replacement.charAt(0).toLocaleUpperCase(locale) + replacement.slice(1); } return replacement; From 9990864f5cdbfa39befa7c8a966d4bb1513187b1 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:06:22 +0000 Subject: [PATCH 013/118] fix: revert changes belonging to parent branch --- i18n/en.pot | 4 ++-- .../metaData/helpers/customLabels/applyCustomTerminology.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7edba62f1e..12c538ef77 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T10:42:54.251Z\n" -"PO-Revision-Date: 2026-08-07T10:42:54.251Z\n" +"POT-Creation-Date: 2026-08-07T11:06:23.919Z\n" +"PO-Revision-Date: 2026-08-07T11:06:23.919Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index faa4ce3765..3d6cd75751 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -28,7 +28,7 @@ const TERM_ENTRIES: ReadonlyArray = ( (form.aliases ?? []).forEach(alias => out.push({ key, plural, english: alias })); }; if (forms.plural) addForm(forms.plural, true); - addForm(forms.singular, false); + if (forms.singular) addForm(forms.singular, false); return out; }).sort((a, b) => b.english.length - a.english.length); From 2e19a93703b125ceeb3c38db3f2e157d95c56486 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:06:58 +0000 Subject: [PATCH 014/118] feat: code clean up --- i18n/en.pot | 4 ++-- .../metaData/helpers/customLabels/customLabels.ts | 1 + .../factory/TrackedEntityType/TrackedEntityTypeFactory.ts | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3cf89134e2..0c4b285d6b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T10:40:46.859Z\n" -"PO-Revision-Date: 2026-08-07T10:40:46.859Z\n" +"POT-Creation-Date: 2026-08-07T11:06:59.807Z\n" +"PO-Revision-Date: 2026-08-07T11:06:59.807Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 8c15d0ad43..a7de3b4643 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -50,6 +50,7 @@ export const CUSTOM_LABEL_FIELDS = { plural: { field: 'displayOrgUnitLabel', english: 'organisation units' }, }, trackedEntityType: { + singular: { field: 'displayTrackedEntityTypeLabel', english: 'tracked entity' }, plural: { field: 'displayTrackedEntityTypesLabel', english: 'tracked entities' }, }, followUp: { diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index 7e5e9d5a91..e5c1359aae 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -84,10 +84,14 @@ export class TrackedEntityTypeFactory { const trackedEntityType = new TrackedEntityType((o) => { o.id = cachedType.id; o.access = cachedType.access; - o.name = this._getTranslation( + const name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.customLabels = extractCustomLabels(cachedType); + o.name = name; + o.customLabels = extractCustomLabels({ + ...cachedType, + displayTrackedEntityTypeLabel: name, + }); }); if (cachedType.trackedEntityTypeAttributes) { From 0a0d90a1a4fd8efd103bd73526bba979834262ce Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:10:18 +0000 Subject: [PATCH 015/118] fix: type change --- i18n/en.pot | 4 ++-- .../metaData/helpers/customLabels/customLabels.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0c4b285d6b..ec13175020 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T11:06:59.807Z\n" -"PO-Revision-Date: 2026-08-07T11:06:59.807Z\n" +"POT-Creation-Date: 2026-08-07T11:10:20.336Z\n" +"PO-Revision-Date: 2026-08-07T11:10:20.336Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index a7de3b4643..6dd96f7757 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -17,7 +17,7 @@ export type CustomLabelForm = { }; export type CustomLabelField = { - singular?: CustomLabelForm, + singular: CustomLabelForm, plural?: CustomLabelForm, }; @@ -65,7 +65,7 @@ export type LabelOptions = { plural?: boolean }; const ALL_FIELDS: ReadonlyArray = Array.from( new Set( Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.singular?.field, term.plural?.field]) + .flatMap((term: CustomLabelField) => [term.singular.field, term.plural?.field]) .filter((field): field is string => Boolean(field)), ), ); From 809aad61a6967f2d502c0cf13edf49ed752222ad Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:44:09 +0000 Subject: [PATCH 016/118] feat: update terminology --- i18n/en.pot | 49 +++++++++---------- .../Filters/FiltersRows.component.tsx | 2 +- .../NewEventWorkspace.component.tsx | 2 +- .../ProgramStageSelector.container.tsx | 2 +- .../TopBar/TopBar.component.tsx | 2 +- .../EnrollmentEditEvent/TopBar.container.tsx | 2 +- .../WidgetEnrollmentEventNew.container.tsx | 2 +- .../DataEntry/editEventDataEntry.actions.ts | 2 +- .../epics/editEventDataEntry.epics.ts | 2 +- .../viewEventDataEntry.actions.ts | 2 +- .../WidgetEventSchedule.container.tsx | 2 +- .../StageCreateNewButton.tsx | 2 +- .../Stages/Stages.component.tsx | 2 +- .../WidgetStagesAndEvents.component.tsx | 2 +- .../TrackedEntityType/TrackedEntityType.ts | 10 ---- .../TrackedEntityTypeFactory.ts | 12 +---- 16 files changed, 38 insertions(+), 59 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ec13175020..d69d57085a 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T11:10:20.336Z\n" -"PO-Revision-Date: 2026-08-07T11:10:20.336Z\n" +"POT-Creation-Date: 2026-08-07T13:44:12.014Z\n" +"PO-Revision-Date: 2026-08-07T13:44:12.017Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -688,8 +688,8 @@ msgstr "before or equal to" msgid "More filters" msgstr "More filters" -msgid "Stage filters" -msgstr "Stage filters" +msgid "Program stage filters" +msgstr "Program stage filters" msgid "Rows per page" msgstr "Rows per page" @@ -811,8 +811,8 @@ msgstr "There was an error loading the page" msgid "Program stage is invalid" msgstr "Program stage is invalid" -msgid "Stage not found" -msgstr "Stage not found" +msgid "Program stage not found" +msgstr "Program stage not found" msgid "Report" msgstr "Report" @@ -832,14 +832,14 @@ msgstr "You can't add any more {{ programStageName }} events" msgid "Cancel without saving" msgstr "Cancel without saving" -msgid "Choose a stage for a new event" -msgstr "Choose a stage for a new event" +msgid "Choose a program stage for a new event" +msgstr "Choose a program stage for a new event" msgid "Program Stages could not be loaded" msgstr "Program Stages could not be loaded" -msgid "Stage" -msgstr "Stage" +msgid "Program stage" +msgstr "Program stage" msgid "The category option is not valid for the selected organisation unit." msgstr "The category option is not valid for the selected organisation unit." @@ -1467,9 +1467,6 @@ msgstr "Add coordinates" msgid "Add area" msgstr "Add area" -msgid "Program stage not found" -msgstr "Program stage not found" - msgid "organisation unit could not be retrieved. Please try again later." msgstr "organisation unit could not be retrieved. Please try again later." @@ -1479,8 +1476,8 @@ msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgid "Saving to {{stageName}} for {{programName}}" msgstr "Saving to {{stageName}} for {{programName}}" -msgid "program or stage is invalid" -msgstr "program or stage is invalid" +msgid "Program or program stage is invalid" +msgstr "Program or program stage is invalid" msgid "Notes about this enrollment" msgstr "Notes about this enrollment" @@ -1497,8 +1494,8 @@ msgstr "Error" msgid "Warning" msgstr "Warning" -msgid "stage not found in rules execution" -msgstr "stage not found in rules execution" +msgid "Program stage not found in rules execution" +msgstr "Program stage not found in rules execution" msgid "Delete event" msgstr "Delete event" @@ -1593,9 +1590,6 @@ msgstr "Event notes" msgid "Write a note about this scheduled event" msgstr "Write a note about this scheduled event" -msgid "Program or stage is invalid" -msgstr "Program or stage is invalid" - msgid "Feedback" msgstr "Feedback" @@ -1754,8 +1748,8 @@ msgstr "Please enter a date" msgid "Please select a valid event" msgstr "Please select a valid event" -msgid "This stage can only have one event" -msgstr "This stage can only have one event" +msgid "This program stage can only have one event" +msgstr "This program stage can only have one event" msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" @@ -1804,11 +1798,11 @@ msgstr "{{ overdueEvents }} overdue" msgid "{{ scheduledEvents }} scheduled" msgstr "{{ scheduledEvents }} scheduled" -msgid "No stages found in this program" -msgstr "No stages found in this program" +msgid "No program stages found in this program" +msgstr "No program stages found in this program" -msgid "Stages and Events" -msgstr "Stages and Events" +msgid "Program stages and Events" +msgstr "Program stages and Events" msgid "An error occurred while unlinking and deleting the event." msgstr "An error occurred while unlinking and deleting the event." @@ -2248,6 +2242,9 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" +msgid "..." +msgstr "..." + msgid "Program not found" msgstr "Program not found" diff --git a/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx b/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx index 97fb2d473f..fcb22ae905 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx +++ b/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx @@ -84,7 +84,7 @@ export const FiltersRowsPlain = ({ <>
-
{i18n.t('Stage filters').toUpperCase()}
+
{i18n.t('Program stage filters').toUpperCase()}
item.additionalColumn)} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx index fd6f703bb5..6122ca58cb 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx @@ -69,7 +69,7 @@ const NewEventWorkspacePlain = ({ if (!stage) { return renderWidget( -
{i18n.t('Stage not found')}
, +
{i18n.t('Program stage not found')}
, ); } diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx index 10e33b8309..1a3442a42b 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx @@ -103,7 +103,7 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId <> {program ? diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx index d5dd0a2dc1..dc2357d1a3 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx @@ -102,7 +102,7 @@ export const TopBar = ({ }, ]} selectedValue="alwaysPreselected" - title={i18n.t('Stage')} + title={i18n.t('Program stage')} isUserInteractionInProgress={isUserInteractionInProgress} /> {programStage && ( diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx index 4e48426865..5c2a625856 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx @@ -30,7 +30,7 @@ export const WidgetEnrollmentEventNew = ({ if (!program || !stage || !(program instanceof TrackerProgram) || isError || !formFoundation) { return (
- {i18n.t('program or stage is invalid')} + {i18n.t('Program or program stage is invalid')}
); } diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts index 6f4b8b9243..10d9a263fb 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts @@ -165,7 +165,7 @@ export const openEventForEditInDataEntry = ({ if (program instanceof TrackerProgram) { const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { - throw Error(i18n.t('stage not found in rules execution')); + throw Error(i18n.t('Program stage not found in rules execution')); } // TODO: Add attributeValues & enrollmentData effects = getApplicableRuleEffectsForTrackerProgram({ diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts index cd0f06fee8..eee466d3b6 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts @@ -56,7 +56,7 @@ const runRulesForEditSingleEvent = async ({ : getStageFromEvent(event)?.stage; if (!stage) { - throw Error(i18n.t('stage not found in rules execution')); + throw Error(i18n.t('Program stage not found in rules execution')); } const foundation = stage.stageForm; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts index ed4ea79daa..8290beb2b5 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts @@ -149,7 +149,7 @@ export const loadViewEventDataEntry = if (program instanceof TrackerProgram) { const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { - throw Error(i18n.t('stage not found in rules execution')); + throw Error(i18n.t('Program stage not found in rules execution')); } effects = getApplicableRuleEffectsForTrackerProgram({ diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx index 2b99d00290..7803a97d33 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx @@ -181,7 +181,7 @@ export const WidgetEventSchedule = ({ if (!program || !stage || !(program instanceof TrackerProgram) || !programStageScheduleConfig) { return (
- {i18n.t('Program or stage is invalid')} + {i18n.t('Program or program stage is invalid')}
); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index 891650a162..f1bd8bea3c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -31,7 +31,7 @@ export const StageCreateNewButton = ({ if (!repeatable && eventCount > 0) { return { isDisabled: true, - tooltipContent: i18n.t('This stage can only have one event'), + tooltipContent: i18n.t('This program stage can only have one event'), }; } return { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index 6ee363c119..bca2944634 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx @@ -52,7 +52,7 @@ export const StagesPlain = ({ if (!readableStages.length) { return (

- {i18n.t('No stages found in this program')} + {i18n.t('No program stages found in this program')}

); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 6a410d71ff..c17deb7e4c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -44,7 +44,7 @@ const WidgetStagesAndEventsPlain = ({ - {i18n.t('Stages and Events')} + {i18n.t('Program stages and Events')} {showWidgetBadge && (
; _searchGroups!: Array; - _customLabels!: CustomLabels; constructor(initFn: ((_this: TrackedEntityType) => void) | null) { this._attributes = []; - this._customLabels = {}; initFn && isFunction(initFn) && initFn(this); } @@ -64,11 +61,4 @@ export class TrackedEntityType { get attributes(): Array { return this._attributes; } - - set customLabels(customLabels: CustomLabels) { - this._customLabels = customLabels; - } - get customLabels(): CustomLabels { - return this._customLabels; - } } diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index e5c1359aae..726731a7f2 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -1,8 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { - TrackedEntityType, - extractCustomLabels, -} from '../../../../metaData'; +import { TrackedEntityType } from '../../../../metaData'; import { DataElementFactory } from './DataElementFactory'; import { TeiRegistrationFactory } from './TeiRegistrationFactory'; import { SearchGroupFactory } from '../../../common/factory'; @@ -84,14 +81,9 @@ export class TrackedEntityTypeFactory { const trackedEntityType = new TrackedEntityType((o) => { o.id = cachedType.id; o.access = cachedType.access; - const name = this._getTranslation( + o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.name = name; - o.customLabels = extractCustomLabels({ - ...cachedType, - displayTrackedEntityTypeLabel: name, - }); }); if (cachedType.trackedEntityTypeAttributes) { From 54d8332969de29ecd43896b34f54c2626d218e6b Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:48:07 +0000 Subject: [PATCH 017/118] feat: enhance API program fields with custom terminology support --- i18n/en.pot | 4 +- .../WidgetProfile/hooks/useApiProgram.ts | 65 +++++++++++-------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d69d57085a..e29a211f1b 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T13:44:12.014Z\n" -"PO-Revision-Date: 2026-08-07T13:44:12.017Z\n" +"POT-Creation-Date: 2026-08-07T13:48:09.545Z\n" +"PO-Revision-Date: 2026-08-07T13:48:09.545Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts index ada6c9dde0..95a875dbe6 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts @@ -1,34 +1,45 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; +import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; -const fields = - 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + - 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + - 'displayIncidentDate,access[*],' + - 'dataEntryForm[id,htmlCode],' + - 'categoryCombo[id,displayName,isDefault,categories[id,displayName]],' + - 'programSections[id,displayFormName,displayDescription,sortOrder,trackedEntityAttributes],' + - 'programRuleVariables[id,displayName,programRuleVariableSourceType,valueType,program[id],' + - 'programStage[id],dataElement[id],trackedEntityAttribute[id],useCodeForOptionSet],' + - 'programStages[id,access,autoGenerateEvent,openAfterEnrollment,generatedByEnrollmentDate,' + - 'reportDateToUse,minDaysFromStart,displayName,description,executionDateLabel,formType,featureType,' + - 'validationStrategy,enableUserAssignment,style,' + +const baseTrackedEntityTypeFields = + 'id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + + 'translations[property,locale,value]'; + +const pluralTrackedEntityTypeFields = 'displayTrackedEntityTypesLabel'; + +const buildFields = (includePluralLabels: boolean) => { + const trackedEntityTypeFields = includePluralLabels + ? `${baseTrackedEntityTypeFields},${pluralTrackedEntityTypeFields}` + : baseTrackedEntityTypeFields; + return 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + + 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + + 'displayIncidentDate,access[*],' + 'dataEntryForm[id,htmlCode],' + - 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]],' + - 'programStageDataElements[compulsory,displayInReports,renderOptionsAsRadio,allowFutureDate,' + - 'renderType[*],dataElement[id,displayName,displayShortName,displayFormName,valueType,' + - 'translations[*],description,optionSetValue,style,optionSet[id,displayName,version,valueType,' + - 'options[id,displayName,code,style, translations]]]]],' + - 'programTrackedEntityAttributes[trackedEntityAttribute[id,displayName,displayShortName,displayFormName,' + - 'displayDescription,valueType,optionSetValue,unique,orgunitScope,pattern,translations[property,locale,value],' + - 'optionSet[id,displayName,version,valueType,options[id,displayName,name,code,style,translations]]],' + - 'displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate],' + - 'trackedEntityType[id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + - 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + - 'translations[property,locale,value]],' + - 'userRoles[id,displayName]'; + 'categoryCombo[id,displayName,isDefault,categories[id,displayName]],' + + 'programSections[id,displayFormName,displayDescription,sortOrder,trackedEntityAttributes],' + + 'programRuleVariables[id,displayName,programRuleVariableSourceType,valueType,program[id],' + + 'programStage[id],dataElement[id],trackedEntityAttribute[id],useCodeForOptionSet],' + + 'programStages[id,access,autoGenerateEvent,openAfterEnrollment,generatedByEnrollmentDate,' + + 'reportDateToUse,minDaysFromStart,displayName,description,executionDateLabel,formType,featureType,' + + 'validationStrategy,enableUserAssignment,style,' + + 'dataEntryForm[id,htmlCode],' + + 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]],' + + 'programStageDataElements[compulsory,displayInReports,renderOptionsAsRadio,allowFutureDate,' + + 'renderType[*],dataElement[id,displayName,displayShortName,displayFormName,valueType,' + + 'translations[*],description,optionSetValue,style,optionSet[id,displayName,version,valueType,' + + 'options[id,displayName,code,style, translations]]]]],' + + 'programTrackedEntityAttributes[trackedEntityAttribute[id,displayName,displayShortName,displayFormName,' + + 'displayDescription,valueType,optionSetValue,unique,orgunitScope,pattern,translations[property,locale,value],' + + 'optionSet[id,displayName,version,valueType,options[id,displayName,name,code,style,translations]]],' + + 'displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate],' + + `trackedEntityType[${trackedEntityTypeFields}],` + + 'userRoles[id,displayName]'; +}; export const useApiProgram = (programId: string) => { + const includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); const { error, loading, data } = useDataQuery( useMemo( () => ({ @@ -36,11 +47,11 @@ export const useApiProgram = (programId: string) => { resource: 'programs', id: programId, params: { - fields, + fields: buildFields(includePluralLabels), }, }, }), - [programId], + [programId, includePluralLabels], ), ); From 45eb553f337c34ecb7c377b26659a67f81ab721c Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:57:53 +0000 Subject: [PATCH 018/118] feat: clean up --- i18n/en.pot | 7 ++--- .../customLabels/applyCustomTerminology.ts | 28 +++---------------- .../bootstrapCustomTerminology.ts | 16 ++--------- .../helpers/customLabels/customLabels.ts | 22 ++------------- .../customLabels/resolveTerminologyContext.ts | 18 ++---------- 5 files changed, 12 insertions(+), 79 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index e29a211f1b..21f6410dbd 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T13:48:09.545Z\n" -"PO-Revision-Date: 2026-08-07T13:48:09.545Z\n" +"POT-Creation-Date: 2026-08-07T13:57:55.501Z\n" +"PO-Revision-Date: 2026-08-07T13:57:55.501Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2242,9 +2242,6 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" -msgid "..." -msgstr "..." - msgid "Program not found" msgstr "Program not found" diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index 3d6cd75751..e431413630 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -1,12 +1,11 @@ import i18n from '@dhis2/d2-i18n'; -import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; +import { programCollection } from '../../../metaDataMemoryStores'; import { CUSTOM_LABEL_FIELDS, resolveCustomLabel } from './customLabels'; import type { CustomLabelKey, CustomLabels, CustomLabelField } from './customLabels'; export type TerminologyContext = { programId?: string, stageId?: string, - trackedEntityTypeId?: string, }; type TermEntry = { @@ -15,10 +14,6 @@ type TermEntry = { english: string, }; -// Derive the flat list of match candidates from CUSTOM_LABEL_FIELDS (single source -// of truth). Includes each form's English word plus any aliases (e.g. "stage" for -// programStage.singular). Sorted longest-first so multi-word forms are tried before -// their sub-strings — the combined regex's alternation then respects that order. const TERM_ENTRIES: ReadonlyArray = ( Object.entries(CUSTOM_LABEL_FIELDS) as ReadonlyArray<[CustomLabelKey, CustomLabelField]> ).flatMap(([key, forms]) => { @@ -58,34 +53,19 @@ const preserveCase = (match: string, replacement: string, locale: string): strin const getLabelSources = ({ programId, stageId, - trackedEntityTypeId, }: TerminologyContext): Array => { const program = programId ? programCollection.get(programId) : undefined; const stage = program && stageId ? program.getStage(stageId) : undefined; - const tet = trackedEntityTypeId ? trackedEntityTypesCollection.get(trackedEntityTypeId) : undefined; - // Precedence: stage overrides program overrides TET. - return [stage?.customLabels, program?.customLabels, tet?.customLabels]; + return [stage?.customLabels, program?.customLabels]; }; -/** - * Substitute DHIS2 terminology (enrollment, event, note, relationship, ...) in a - * translated string with per-program custom labels when configured. Case in the - * source string is preserved on the substituted term. Locale-aware via `i18n.language`. - * - * @example - * applyCustomTerminology(i18n.t('Write a note about this enrollment'), { programId }) - */ export const applyCustomTerminology = ( translatedText: string, context: TerminologyContext = {}, ): string => { - // Defensive: t() can return non-string values (arrays/objects when - // returnObjects: true, undefined for missing keys with certain configs). - // Only strings go through the substitution pipeline; everything else is - // returned as-is so callers see the original i18next output unchanged. if (typeof translatedText !== 'string' || !translatedText) return translatedText; - const { programId, stageId, trackedEntityTypeId } = context; - if (!programId && !stageId && !trackedEntityTypeId) return translatedText; + const { programId, stageId } = context; + if (!programId && !stageId) return translatedText; const sources = getLabelSources(context); const locale = i18n.language || 'en'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts index 12eba2b70c..67b79af692 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts @@ -2,29 +2,17 @@ import i18n from '@dhis2/d2-i18n'; import { applyCustomTerminology } from './applyCustomTerminology'; import { resolveTerminologyContext } from './resolveTerminologyContext'; -type StoreLike = { getState: () => unknown }; +type ReduxStore = { getState: () => unknown }; let bootstrapped = false; -/** - * Wraps i18n.t so every call passes its result through applyCustomTerminology, - * substituting DHIS2 terms with the current program's custom labels. Program / - * stage / tracked entity type ids are resolved per call via - * resolveTerminologyContext (URL first, then Redux domain state). - * - * Callers can opt out per call with i18n.t(key, { postProcess: false }) — for - * strings that mention DHIS2 terms in the everyday sense. - * - * Call once at app bootstrap. - */ -export const bootstrapCustomTerminology = (store: StoreLike) => { +export const bootstrapCustomTerminology = (store: ReduxStore) => { if (bootstrapped) return; bootstrapped = true; const originalT = i18n.t.bind(i18n); i18n.t = (key: string, options?: any) => { const translated = originalT(key, options); - if (options?.postProcess === false) return translated; return applyCustomTerminology(translated, resolveTerminologyContext(store)); }; }; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 6dd96f7757..530add6dea 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,15 +1,3 @@ -// Owns the shape and lookup of program/stage/tracked-entity-type custom labels: -// - CUSTOM_LABEL_FIELDS is the single source of truth: for each DHIS2 term it -// names the API field to read (per singular/plural), the English word to -// look for in translated strings, and any aliases. -// - extractCustomLabels reads them off the API cache when domain objects are -// built (ProgramFactory / ProgramStageFactory / TrackedEntityTypeFactory). -// - resolveCustomLabel picks a label from those sources at substitution time -// (called by the postProcessor in applyCustomTerminology). -// Previously also exported resolveLabel/getProgramLabel/useProgramLabel which -// forced capitalization at every call site; that pattern is gone since the -// postProcessor now handles case at the point of substitution. - export type CustomLabelForm = { field: string, english: string, @@ -40,19 +28,13 @@ export const CUSTOM_LABEL_FIELDS = { singular: { field: 'displayTrackedEntityAttributeLabel', english: 'attribute' }, }, programStage: { - singular: { field: 'displayProgramStageLabel', english: 'program stage', aliases: ['stage'] }, - plural: { field: 'displayProgramStagesLabel', english: 'program stages', aliases: ['stages'] }, + singular: { field: 'displayProgramStageLabel', english: 'program stage' }, + plural: { field: 'displayProgramStagesLabel', english: 'program stages' }, }, - // API only exposes a singular custom label for orgUnit; we reuse it for the - // plural form so "organisation units" resolves to the same admin-set string. orgUnit: { singular: { field: 'displayOrgUnitLabel', english: 'organisation unit' }, plural: { field: 'displayOrgUnitLabel', english: 'organisation units' }, }, - trackedEntityType: { - singular: { field: 'displayTrackedEntityTypeLabel', english: 'tracked entity' }, - plural: { field: 'displayTrackedEntityTypesLabel', english: 'tracked entities' }, - }, followUp: { singular: { field: 'displayFollowUpLabel', english: 'follow-up' }, }, diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts index a882d38430..ada547f6c1 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts @@ -1,7 +1,7 @@ import { getLocationQuery } from '../../../utils/routing'; import type { TerminologyContext } from './applyCustomTerminology'; -type StoreLike = { getState: () => unknown }; +type ReduxStore = { getState: () => unknown }; type DomainState = { viewEventPage?: { @@ -14,29 +14,16 @@ type DomainState = { }, }; -/** - * Layered resolution of the "current view's program" for terminology substitution. - * 1. URL query — most program-scoped pages carry programId directly (enrollment - * dashboard, working lists, new enrollment, etc.). - * 2. Redux domain state — pages that carry only entity ids (event edit's eventId, - * viewEvent's viewEventId, TEI dashboard's teiId) resolve program via the - * loaded entity. - * 3. Nothing — English fallback. Never touches state.currentSelections, which is - * the top-nav scope filter and can diverge from the entity actually on screen. - */ -export const resolveTerminologyContext = (store: StoreLike): TerminologyContext => { +export const resolveTerminologyContext = (store: ReduxStore): TerminologyContext => { const query = getLocationQuery(); - // Layer 1: URL if (query.programId) { return { programId: query.programId, stageId: query.stageId ?? query.programStageId, - trackedEntityTypeId: query.trackedEntityTypeId, }; } - // Layer 2: Redux domain state const state = (store.getState() ?? {}) as DomainState; if (query.eventId || query.viewEventId) { @@ -53,6 +40,5 @@ export const resolveTerminologyContext = (store: StoreLike): TerminologyContext } } - // Layer 3: no context — postProcessor will leave the string untouched. return {}; }; From de452bc429c93e2d8f71deb3cea13ac3382778cc Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:19:54 +0000 Subject: [PATCH 019/118] feat: temp devin review --- i18n/en.pot | 30 ++++++++-------- .../NotesSection/NotesSection.component.tsx | 4 +-- .../RelationshipsSection.component.tsx | 4 +-- .../WidgetBreakingTheGlass.component.tsx | 12 +++---- .../Status/Status.component.tsx | 4 +-- .../constants/status.const.ts | 2 +- .../customLabels/applyCustomTerminology.ts | 19 +++++++++- .../bootstrapCustomTerminology.ts | 35 +++++++++++++++++-- .../customLabels/resolveTerminologyContext.ts | 8 ++--- src/declarations.d.ts | 2 -- 10 files changed, 80 insertions(+), 40 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 21f6410dbd..f31db02f11 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-07T13:57:55.501Z\n" -"PO-Revision-Date: 2026-08-07T13:57:55.501Z\n" +"POT-Creation-Date: 2026-08-12T11:19:55.588Z\n" +"PO-Revision-Date: 2026-08-12T11:19:55.588Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1306,22 +1306,12 @@ msgstr "No one is assigned to this event" msgid "Assign" msgstr "Assign" -msgid "This program is protected" -msgstr "This program is protected" - -msgid "Reason to check for enrollments" -msgstr "Reason to check for enrollments" - -msgid "" -"Describe the reason you are checking for enrollments in this protected " -"program" -msgstr "" -"Describe the reason you are checking for enrollments in this protected " -"program" - msgid "Check for enrollments" msgstr "Check for enrollments" +msgid "This program is protected" +msgstr "This program is protected" + msgid "" "You must provide a reason to check for enrollments in this protected " "program." @@ -1332,6 +1322,16 @@ msgstr "" msgid "All activity will be logged." msgstr "All activity will be logged." +msgid "Reason to check for enrollments" +msgstr "Reason to check for enrollments" + +msgid "" +"Describe the reason you are checking for enrollments in this protected " +"program" +msgstr "" +"Describe the reason you are checking for enrollments in this protected " +"program" + msgid "Unsaved changes" msgstr "Unsaved changes" diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx index bcd0546089..aba080dd1b 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx @@ -12,8 +12,6 @@ import type { PlainProps } from './NotesSection.types'; const LoadingNotes = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Notes); -const headerText = i18n.t('Notes'); - const getStyles = (theme: any) => ({ badge: { backgroundColor: theme.palette.grey.light, @@ -42,7 +40,7 @@ class NotesSectionPlain extends React.Component { return ( diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx index dd3189f132..b9f3302044 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx @@ -15,8 +15,6 @@ import type { PlainProps } from './RelationshipsSection.types'; const LoadingRelationships = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Relationships); -const headerText = i18n.t('Relationships'); - const getStyles = (theme: any) => ({ badge: { backgroundColor: theme.palette.grey.light, @@ -53,7 +51,7 @@ class RelationshipsSectionPlain extends React.Component { return ( diff --git a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx index 79c8583d90..494b3c5ba1 100644 --- a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx +++ b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx @@ -23,10 +23,6 @@ const styles: Readonly = ({ typography }: any) => ({ }, }); -const noticeBoxTitle = i18n.t('This program is protected'); -const reasonHeader = i18n.t('Reason to check for enrollments'); -const reasonPlaceholder = i18n.t('Describe the reason you are checking for enrollments in this protected program'); - type Props = PlainProps & WithStyles; const WidgetBreakingTheGlassPlain = ({ @@ -52,15 +48,17 @@ const WidgetBreakingTheGlassPlain = ({ {i18n.t('Check for enrollments')}

- + {i18n.t('You must provide a reason to check for enrollments in this protected program.')} {' '} {i18n.t('All activity will be logged.')}
- {translatedStatus[status] ?? status} + {getTranslatedStatus()[status] ?? status} ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/constants/status.const.ts b/src/core_modules/capture-core/components/WidgetEnrollment/constants/status.const.ts index 2b0bb8939f..a8e0e23d69 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/constants/status.const.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/constants/status.const.ts @@ -6,7 +6,7 @@ export const plainStatus = Object.freeze({ CANCELLED: 'CANCELLED', }); -export const translatedStatus = Object.freeze({ +export const getTranslatedStatus = () => ({ [plainStatus.ACTIVE]: i18n.t('Active'), [plainStatus.COMPLETED]: i18n.t('Completed'), [plainStatus.CANCELLED]: i18n.t('Cancelled'), diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index e431413630..70002ff2fe 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -8,6 +8,15 @@ export type TerminologyContext = { stageId?: string, }; +// Unicode "non-characters" (U+FDD0 / U+FDD1) — the Unicode standard reserves +// these to never be used for text, so they never collide with real content. +// The bootstrap wrapper brackets every interpolated value with these markers +// so we can skip them here and avoid rewriting server-supplied names that +// happen to contain a token word (e.g. a stage named "Birth event"). +export const INTERPOLATION_OPEN = '﷐'; +export const INTERPOLATION_CLOSE = '﷑'; +const INTERPOLATION_PATTERN = new RegExp(`${INTERPOLATION_OPEN}(.*?)${INTERPOLATION_CLOSE}`, 'g'); + type TermEntry = { key: CustomLabelKey, plural: boolean, @@ -70,11 +79,19 @@ export const applyCustomTerminology = ( const sources = getLabelSources(context); const locale = i18n.language || 'en'; - return translatedText.replace(COMBINED_PATTERN, (match) => { + const substitute = (text: string): string => text.replace(COMBINED_PATTERN, (match) => { const entry = findEntry(match); if (!entry) return match; const custom = resolveCustomLabel(sources, entry.key, { plural: entry.plural }); if (!custom) return match; return preserveCase(match, custom, locale); }); + + // Split on sentinel-wrapped interpolation regions. String.split with a + // capturing group returns [outside, inside, outside, ...] — substitute + // only in the outside parts so server-supplied values pass through + // unchanged. If no sentinels are present, we get [translatedText] and + // just substitute the whole thing. + const parts = translatedText.split(INTERPOLATION_PATTERN); + return parts.map((part, i) => (i % 2 === 0 ? substitute(part) : part)).join(''); }; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts index 67b79af692..8635a28e58 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts @@ -1,9 +1,40 @@ import i18n from '@dhis2/d2-i18n'; -import { applyCustomTerminology } from './applyCustomTerminology'; +import { applyCustomTerminology, INTERPOLATION_OPEN, INTERPOLATION_CLOSE } from './applyCustomTerminology'; import { resolveTerminologyContext } from './resolveTerminologyContext'; type ReduxStore = { getState: () => unknown }; +const I18N_CONTROL_KEYS = new Set([ + 'context', + 'count', + 'defaultValue', + 'fallbackLng', + 'interpolation', + 'joinArrays', + 'keySeparator', + 'lng', + 'lngs', + 'ns', + 'nsSeparator', + 'postProcess', + 'replace', + 'returnDetails', + 'returnObjects', + 'skipInterpolation', +]); + +const wrapInterpolationValues = (options?: Record): Record | undefined => { + if (!options || typeof options !== 'object') return options; + const wrapped: Record = {}; + for (const key of Object.keys(options)) { + const value = options[key]; + wrapped[key] = typeof value === 'string' && !I18N_CONTROL_KEYS.has(key) + ? `${INTERPOLATION_OPEN}${value}${INTERPOLATION_CLOSE}` + : value; + } + return wrapped; +}; + let bootstrapped = false; export const bootstrapCustomTerminology = (store: ReduxStore) => { @@ -12,7 +43,7 @@ export const bootstrapCustomTerminology = (store: ReduxStore) => { const originalT = i18n.t.bind(i18n); i18n.t = (key: string, options?: any) => { - const translated = originalT(key, options); + const translated = originalT(key, wrapInterpolationValues(options)); return applyCustomTerminology(translated, resolveTerminologyContext(store)); }; }; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts index ada547f6c1..cb0540e42f 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts @@ -6,7 +6,7 @@ type ReduxStore = { getState: () => unknown }; type DomainState = { viewEventPage?: { loadedValues?: { - eventContainer?: { event?: { program?: string, programStage?: string } }, + eventContainer?: { event?: { programId?: string, programStageId?: string } }, }, }, enrollmentDomain?: { @@ -26,10 +26,10 @@ export const resolveTerminologyContext = (store: ReduxStore): TerminologyContext const state = (store.getState() ?? {}) as DomainState; - if (query.eventId || query.viewEventId) { + if (query.eventId) { const event = state.viewEventPage?.loadedValues?.eventContainer?.event; - if (event?.program) { - return { programId: event.program, stageId: event.programStage }; + if (event?.programId) { + return { programId: event.programId, stageId: event.programStageId }; } } diff --git a/src/declarations.d.ts b/src/declarations.d.ts index b3de0cd798..561b1762ee 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -13,8 +13,6 @@ declare module 'src/core_modules/*'; declare module '@dhis2/d2-i18n' { const i18n: { t: (key: string, options?: any) => any; - // Added for applyCustomTerminology, which passes the active locale to - // toLocaleUpperCase/toLocaleLowerCase for correct casing (e.g. Turkish i/İ). language: string; // Add other methods as needed }; From 6a8cd9c03d83319ee50323115b1a5fedc05f0a91 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:22:50 +0000 Subject: [PATCH 020/118] fix: revert single event infrastructure --- i18n/en.pot | 4 ++-- .../customLabels/resolveTerminologyContext.ts | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index f31db02f11..9d63eb5c1c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T11:19:55.588Z\n" -"PO-Revision-Date: 2026-08-12T11:19:55.588Z\n" +"POT-Creation-Date: 2026-08-12T11:22:51.758Z\n" +"PO-Revision-Date: 2026-08-12T11:22:51.758Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts index cb0540e42f..07c2156337 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts @@ -4,11 +4,6 @@ import type { TerminologyContext } from './applyCustomTerminology'; type ReduxStore = { getState: () => unknown }; type DomainState = { - viewEventPage?: { - loadedValues?: { - eventContainer?: { event?: { programId?: string, programStageId?: string } }, - }, - }, enrollmentDomain?: { enrollment?: { program?: string }, }, @@ -26,13 +21,6 @@ export const resolveTerminologyContext = (store: ReduxStore): TerminologyContext const state = (store.getState() ?? {}) as DomainState; - if (query.eventId) { - const event = state.viewEventPage?.loadedValues?.eventContainer?.event; - if (event?.programId) { - return { programId: event.programId, stageId: event.programStageId }; - } - } - if (query.enrollmentId || query.teiId) { const enrollment = state.enrollmentDomain?.enrollment; if (enrollment?.program) { From 56ac5566db46f5b70a486e6370026dbbf341908c Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:20:38 +0000 Subject: [PATCH 021/118] feat: update terminology handling across components --- i18n/en.pot | 4 +- .../useTeiDisplayName.ts | 4 +- .../Relationships/Relationships.component.tsx | 7 +- .../WidgetProfile/hooks/useTeiDisplayName.ts | 6 +- .../utils/getDataEntryDetails.ts | 68 +++++++++---------- .../trackedEntityInstances/getDisplayName.ts | 4 +- 6 files changed, 44 insertions(+), 49 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9d63eb5c1c..d59d365cad 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T11:22:51.758Z\n" -"PO-Revision-Date: 2026-08-12T11:22:51.758Z\n" +"POT-Creation-Date: 2026-08-12T12:20:40.672Z\n" +"PO-Revision-Date: 2026-08-12T12:20:40.672Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useTeiDisplayName.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useTeiDisplayName.ts index 06813db015..b07e923acf 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useTeiDisplayName.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/useTeiDisplayName.ts @@ -6,8 +6,6 @@ import { getAttributesFromScopeId } from '../../../../metaData/helpers'; import type { DataElement } from '../../../../metaData/DataElement'; import { convertServerToClient, convertClientToView } from '../../../../converters'; -const DEFAULT_NAME = i18n.t('tracked entity instance'); - type Attribute = { valueType: string; attribute: string; @@ -39,7 +37,7 @@ const getTetAttributes = (attributes: Array, tetAttributes: Array, trackedEntityType: string, teiId: string) => { const tetAttributes = getAttributesFromScopeId(trackedEntityType); - if (!attributes || !tetAttributes) return teiId ?? DEFAULT_NAME; + if (!attributes || !tetAttributes) return teiId ?? i18n.t('tracked entity instance'); const teiNameDisplayInReports = getTetAttributesDisplayInReports(attributes, tetAttributes); if (teiNameDisplayInReports) return teiNameDisplayInReports; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index 0762e0d941..7b8ccb42e9 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -63,8 +63,9 @@ const styles: Readonly = (theme: any) => ({ }, }); -const fromNames = { - PROGRAM_STAGE_INSTANCE: i18n.t('This event'), +const getFromName = (entityType: string) => { + if (entityType === 'PROGRAM_STAGE_INSTANCE') return i18n.t('This event'); + return undefined; }; type PlainProps = { @@ -105,7 +106,7 @@ class RelationshipsPlain extends React.Component { const { onRenderConnectedEntity } = this.props; if (entity.id === this.props.currentEntityId) { - return fromNames[entity.type]; + return getFromName(entity.type); } return onRenderConnectedEntity ? onRenderConnectedEntity(entity) : entity.name; diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useTeiDisplayName.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useTeiDisplayName.ts index 0b8f9ec300..dec5ad533c 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useTeiDisplayName.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useTeiDisplayName.ts @@ -2,8 +2,6 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { convertClientToView } from '../DataEntry'; -const DEFAULT_NAME = i18n.t('tracked entity instance'); - type TeiAttribute = { attribute: string; value?: string; @@ -49,7 +47,7 @@ const deriveTeiName = ( tetAttributes: TetAttribute[], teiId?: string, ) => { - if (!attributes || !tetAttributes) return teiId ?? DEFAULT_NAME; + if (!attributes || !tetAttributes) return teiId ?? i18n.t('tracked entity instance'); const teiNameDisplayInList = getTetAttributesDisplayInList(attributes, tetAttributes as TetAttribute[]); if (teiNameDisplayInList) return teiNameDisplayInList; @@ -57,7 +55,7 @@ const deriveTeiName = ( const teiName = getTetAttributes(attributes, tetAttributes); if (teiName) return teiName; - return teiId ?? DEFAULT_NAME; + return teiId ?? i18n.t('tracked entity instance'); }; export const useTeiDisplayName = ( diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts index 892b22a4a0..7d5c10dbf1 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts @@ -13,42 +13,42 @@ export const Placements = { BOTTOM: 'BOTTOM', }; -const StatusLabels = { - ACTIVE: i18n.t('Active'), - COMPLETED: i18n.t('Completed'), - CANCELLED: i18n.t('Cancelled'), - SCHEDULE: i18n.t('Scheduled'), -}; +export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: RenderFoundation) => { + const statusLabels: Record = { + ACTIVE: i18n.t('Active'), + COMPLETED: i18n.t('Completed'), + CANCELLED: i18n.t('Cancelled'), + SCHEDULE: i18n.t('Scheduled'), + }; -const DataEntryFieldsToInclude = { - occurredAt: { - apiKey: 'occurredAt', - type: dataElementTypes.DATE, - placement: Placements.TOP, - }, - scheduledAt: { - apiKey: 'scheduledAt', - type: dataElementTypes.DATE, - placement: Placements.TOP, - }, - orgUnit: { - apiKey: 'orgUnit', - type: dataElementTypes.ORGANISATION_UNIT, - placement: Placements.TOP, - label: i18n.t('Organisation unit'), - convertFn: (orgUnitId: string) => React.createElement(TooltipOrgUnit, { orgUnitId }), - }, - status: { - apiKey: 'status', - type: dataElementTypes.TEXT, - placement: Placements.BOTTOM, - label: i18n.t('Status'), - convertFn: (value: keyof typeof StatusLabels) => StatusLabels[value], - }, -}; + const dataEntryFieldsToInclude = { + occurredAt: { + apiKey: 'occurredAt', + type: dataElementTypes.DATE, + placement: Placements.TOP, + }, + scheduledAt: { + apiKey: 'scheduledAt', + type: dataElementTypes.DATE, + placement: Placements.TOP, + }, + orgUnit: { + apiKey: 'orgUnit', + type: dataElementTypes.ORGANISATION_UNIT, + placement: Placements.TOP, + label: i18n.t('Organisation unit'), + convertFn: (orgUnitId: string) => React.createElement(TooltipOrgUnit, { orgUnitId }), + }, + status: { + apiKey: 'status', + type: dataElementTypes.TEXT, + placement: Placements.BOTTOM, + label: i18n.t('Status'), + convertFn: (value: string) => statusLabels[value], + }, + }; -export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: RenderFoundation) => { - const dataEntryValues = Object.values(DataEntryFieldsToInclude).map((entry: any) => { + const dataEntryValues = Object.values(dataEntryFieldsToInclude).map((entry: any) => { const value = linkedEvent[entry.apiKey]; if (!value) return null; diff --git a/src/core_modules/capture-core/trackedEntityInstances/getDisplayName.ts b/src/core_modules/capture-core/trackedEntityInstances/getDisplayName.ts index e70af09e5d..577af518e8 100644 --- a/src/core_modules/capture-core/trackedEntityInstances/getDisplayName.ts +++ b/src/core_modules/capture-core/trackedEntityInstances/getDisplayName.ts @@ -2,8 +2,6 @@ import i18n from '@dhis2/d2-i18n'; import type { DataElement } from '../metaData'; import { convertClientToView } from '../converters'; -const DEFAULT_NAME = i18n.t('tracked entity instance'); - export function getDisplayName( values: { [attrId: string]: any }, attributes: Array, @@ -13,7 +11,7 @@ export function getDisplayName( const displayValues = attributes.filter(a => valueIds.some(id => id === a.id) && a.displayInReports); if (displayValues.length === 0) { - return fallbackName || DEFAULT_NAME; + return fallbackName || i18n.t('tracked entity instance'); } return displayValues.slice(0, 2) From 718ae465453b49f8f2b4824e5f7ac9e10009bd64 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:26:43 +0000 Subject: [PATCH 022/118] feat: enhance custom terminology handling and optimize location query caching --- i18n/en.pot | 4 ++-- .../customLabels/applyCustomTerminology.ts | 12 ++++++---- .../bootstrapCustomTerminology.ts | 11 ++++++++- .../utils/routing/getLocationQuery.ts | 24 +++++++++++++++---- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d59d365cad..e3911f7bb7 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T12:20:40.672Z\n" -"PO-Revision-Date: 2026-08-12T12:20:40.672Z\n" +"POT-Creation-Date: 2026-08-12T12:26:45.741Z\n" +"PO-Revision-Date: 2026-08-12T12:26:45.741Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts index 70002ff2fe..1c2ce47edb 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts @@ -38,10 +38,14 @@ const TERM_ENTRIES: ReadonlyArray = ( const escapeRegExp = (input: string): string => input.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); -const COMBINED_PATTERN = new RegExp( - String.raw`\b(${TERM_ENTRIES.map(entry => escapeRegExp(entry.english)).join('|')})\b`, - 'gi', -); +const COMBINED_PATTERN_SOURCE = String.raw`\b(${TERM_ENTRIES.map(entry => escapeRegExp(entry.english)).join('|')})\b`; +const COMBINED_PATTERN = new RegExp(COMBINED_PATTERN_SOURCE, 'gi'); +// Non-global variant used only for the wrapper fast-path — `.test` on a +// global regex is stateful (advances lastIndex), which we want to avoid. +const HAS_ANY_TOKEN_PATTERN = new RegExp(COMBINED_PATTERN_SOURCE, 'i'); + +export const hasCustomTerminologyTokens = (text: string): boolean => + typeof text === 'string' && HAS_ANY_TOKEN_PATTERN.test(text); const findEntry = (match: string): TermEntry | undefined => { const lower = match.toLowerCase(); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts index 8635a28e58..69750a61c3 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts @@ -1,5 +1,10 @@ import i18n from '@dhis2/d2-i18n'; -import { applyCustomTerminology, INTERPOLATION_OPEN, INTERPOLATION_CLOSE } from './applyCustomTerminology'; +import { + applyCustomTerminology, + hasCustomTerminologyTokens, + INTERPOLATION_OPEN, + INTERPOLATION_CLOSE, +} from './applyCustomTerminology'; import { resolveTerminologyContext } from './resolveTerminologyContext'; type ReduxStore = { getState: () => unknown }; @@ -44,6 +49,10 @@ export const bootstrapCustomTerminology = (store: ReduxStore) => { const originalT = i18n.t.bind(i18n); i18n.t = (key: string, options?: any) => { const translated = originalT(key, wrapInterpolationValues(options)); + // Skip context resolution + regex replace when no terminology token + // appears anywhere in the translated string — the common case for + // most UI strings (buttons, dates, generic labels). + if (!hasCustomTerminologyTokens(translated)) return translated; return applyCustomTerminology(translated, resolveTerminologyContext(store)); }; }; diff --git a/src/core_modules/capture-core/utils/routing/getLocationQuery.ts b/src/core_modules/capture-core/utils/routing/getLocationQuery.ts index b3870d1b2d..322d6ba38d 100644 --- a/src/core_modules/capture-core/utils/routing/getLocationQuery.ts +++ b/src/core_modules/capture-core/utils/routing/getLocationQuery.ts @@ -1,7 +1,21 @@ +// Cached across calls — window.location.hash doesn't change mid-render, but +// this is called from hot paths (customLabels wrapper, epics) that would +// otherwise re-parse the URL and allocate a new object on every invocation. +let cachedHash: string | undefined; +let cachedQuery: Readonly> | undefined; + export const getLocationQuery = (): any => { - const urlSearchParamString = window.location.hash.split('?')[1]; - return [...new URLSearchParams(urlSearchParamString).entries()].reduce((accParams, [key, value]) => { - accParams[key] = value; - return accParams; - }, {}); + const hash = window.location.hash; + if (cachedQuery && hash === cachedHash) return cachedQuery; + const urlSearchParamString = hash.split('?')[1]; + const query = [...new URLSearchParams(urlSearchParamString).entries()].reduce>( + (accParams, [key, value]) => { + accParams[key] = value; + return accParams; + }, + {}, + ); + cachedHash = hash; + cachedQuery = Object.freeze(query); + return cachedQuery; }; From b52ffd8dc6c6696b1e6d986b2fdda75b5aaff32f Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:43:52 +0000 Subject: [PATCH 023/118] feat: custom terminology context handling --- i18n/en.pot | 4 +- .../WidgetProfile/hooks/useApiProgram.ts | 60 ++++++++----------- .../useGroupedLinkedEntities.ts | 14 ++++- .../bootstrapCustomTerminology.ts | 26 +++++--- .../metaData/helpers/customLabels/index.ts | 2 + .../customLabels/programTerminologyContext.ts | 37 ++++++++++++ .../customLabels/resolveTerminologyContext.ts | 30 +++++----- .../helpers/customLabels/useProgramT.ts | 24 ++++++++ .../storeTrackedEntityTypes.ts | 14 ++--- .../utils/routing/getLocationQuery.ts | 24 ++------ src/declarations.d.ts | 3 +- 11 files changed, 149 insertions(+), 89 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts diff --git a/i18n/en.pot b/i18n/en.pot index e3911f7bb7..6c6b4d23cd 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T12:26:45.741Z\n" -"PO-Revision-Date: 2026-08-12T12:26:45.741Z\n" +"POT-Creation-Date: 2026-08-12T13:43:54.110Z\n" +"PO-Revision-Date: 2026-08-12T13:43:54.110Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts index 95a875dbe6..97fae559f4 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts @@ -1,45 +1,37 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; -import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; -const baseTrackedEntityTypeFields = +const trackedEntityTypeFields = 'id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + 'translations[property,locale,value]'; -const pluralTrackedEntityTypeFields = 'displayTrackedEntityTypesLabel'; - -const buildFields = (includePluralLabels: boolean) => { - const trackedEntityTypeFields = includePluralLabels - ? `${baseTrackedEntityTypeFields},${pluralTrackedEntityTypeFields}` - : baseTrackedEntityTypeFields; - return 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + - 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + - 'displayIncidentDate,access[*],' + +const buildFields = (): string => + 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + + 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + + 'displayIncidentDate,access[*],' + + 'dataEntryForm[id,htmlCode],' + + 'categoryCombo[id,displayName,isDefault,categories[id,displayName]],' + + 'programSections[id,displayFormName,displayDescription,sortOrder,trackedEntityAttributes],' + + 'programRuleVariables[id,displayName,programRuleVariableSourceType,valueType,program[id],' + + 'programStage[id],dataElement[id],trackedEntityAttribute[id],useCodeForOptionSet],' + + 'programStages[id,access,autoGenerateEvent,openAfterEnrollment,generatedByEnrollmentDate,' + + 'reportDateToUse,minDaysFromStart,displayName,description,executionDateLabel,formType,featureType,' + + 'validationStrategy,enableUserAssignment,style,' + 'dataEntryForm[id,htmlCode],' + - 'categoryCombo[id,displayName,isDefault,categories[id,displayName]],' + - 'programSections[id,displayFormName,displayDescription,sortOrder,trackedEntityAttributes],' + - 'programRuleVariables[id,displayName,programRuleVariableSourceType,valueType,program[id],' + - 'programStage[id],dataElement[id],trackedEntityAttribute[id],useCodeForOptionSet],' + - 'programStages[id,access,autoGenerateEvent,openAfterEnrollment,generatedByEnrollmentDate,' + - 'reportDateToUse,minDaysFromStart,displayName,description,executionDateLabel,formType,featureType,' + - 'validationStrategy,enableUserAssignment,style,' + - 'dataEntryForm[id,htmlCode],' + - 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]],' + - 'programStageDataElements[compulsory,displayInReports,renderOptionsAsRadio,allowFutureDate,' + - 'renderType[*],dataElement[id,displayName,displayShortName,displayFormName,valueType,' + - 'translations[*],description,optionSetValue,style,optionSet[id,displayName,version,valueType,' + - 'options[id,displayName,code,style, translations]]]]],' + - 'programTrackedEntityAttributes[trackedEntityAttribute[id,displayName,displayShortName,displayFormName,' + - 'displayDescription,valueType,optionSetValue,unique,orgunitScope,pattern,translations[property,locale,value],' + - 'optionSet[id,displayName,version,valueType,options[id,displayName,name,code,style,translations]]],' + - 'displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate],' + - `trackedEntityType[${trackedEntityTypeFields}],` + - 'userRoles[id,displayName]'; -}; + 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]],' + + 'programStageDataElements[compulsory,displayInReports,renderOptionsAsRadio,allowFutureDate,' + + 'renderType[*],dataElement[id,displayName,displayShortName,displayFormName,valueType,' + + 'translations[*],description,optionSetValue,style,optionSet[id,displayName,version,valueType,' + + 'options[id,displayName,code,style, translations]]]]],' + + 'programTrackedEntityAttributes[trackedEntityAttribute[id,displayName,displayShortName,displayFormName,' + + 'displayDescription,valueType,optionSetValue,unique,orgunitScope,pattern,translations[property,locale,value],' + + 'optionSet[id,displayName,version,valueType,options[id,displayName,name,code,style,translations]]],' + + 'displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate],' + + `trackedEntityType[${trackedEntityTypeFields}],` + + 'userRoles[id,displayName]'; export const useApiProgram = (programId: string) => { - const includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); const { error, loading, data } = useDataQuery( useMemo( () => ({ @@ -47,11 +39,11 @@ export const useApiProgram = (programId: string) => { resource: 'programs', id: programId, params: { - fields: buildFields(includePluralLabels), + fields: buildFields(), }, }, }), - [programId, includePluralLabels], + [programId], ), ); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts index bcd69e09cb..8a54b9b721 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts @@ -4,11 +4,17 @@ import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { errorCreator } from 'capture-core-utils'; import { dataElementTypes } from '../../../../metaData'; +import { withProgramTerminologyContext } from '../../../../metaData/helpers/customLabels'; import { RELATIONSHIP_ENTITIES } from '../constants'; import { convertClientToList, convertServerToClient } from '../../../../converters'; import type { GroupedLinkedEntities, LinkedEntityData } from './types'; import type { ApiLinkedEntity, InputRelationshipData, RelationshipTypes } from '../Types'; +const getConstraintTerminologyContext = (constraint: any) => ({ + programId: constraint.program?.id, + stageId: 'programStage' in constraint ? constraint.programStage.id : undefined, +}); + const getFallbackFieldsByRelationshipEntity = { [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ id: 'trackedEntityTypeName', @@ -224,7 +230,13 @@ export const useGroupedLinkedEntities = ( { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; - const columns = getColumns(constraint); + // Use the constraint's own program for terminology so that + // column headers (e.g. "Program stage name") reflect the + // linked program's labels, not the currently-selected program. + const columns = withProgramTerminologyContext( + getConstraintTerminologyContext(constraint), + () => getColumns(constraint), + ); const context = getContext(constraint, relationshipType.access, readOnly); accGroupedLinkedEntities.push({ diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts index 69750a61c3..6d20034855 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts @@ -46,13 +46,23 @@ export const bootstrapCustomTerminology = (store: ReduxStore) => { if (bootstrapped) return; bootstrapped = true; + // Register terminology replacement as an i18next postProcessor plugin so it + // uses the framework's documented extension point rather than overwriting t. + i18n.use({ + type: 'postProcessor' as const, + name: 'customTerminology', + process(value: string): string { + if (!hasCustomTerminologyTokens(value)) return value; + return applyCustomTerminology(value, resolveTerminologyContext(store)); + }, + }); + // Enable the plugin globally — i18next reads this from options at call time. + (i18n.options as any).postProcess = 'customTerminology'; + + // Thin intercept solely to bracket interpolated values with sentinel markers + // before i18next performs interpolation. This prevents terminology replacement + // from rewriting server-supplied names (e.g. a stage called "Birth event"). + // No equivalent pre-interpolation hook exists in the i18next plugin API. const originalT = i18n.t.bind(i18n); - i18n.t = (key: string, options?: any) => { - const translated = originalT(key, wrapInterpolationValues(options)); - // Skip context resolution + regex replace when no terminology token - // appears anywhere in the translated string — the common case for - // most UI strings (buttons, dates, generic labels). - if (!hasCustomTerminologyTokens(translated)) return translated; - return applyCustomTerminology(translated, resolveTerminologyContext(store)); - }; + i18n.t = (key: string, options?: any) => originalT(key, wrapInterpolationValues(options)); }; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index 06a2a4cd5d..499df058c3 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -7,3 +7,5 @@ export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels' export { applyCustomTerminology } from './applyCustomTerminology'; export type { TerminologyContext } from './applyCustomTerminology'; export { bootstrapCustomTerminology } from './bootstrapCustomTerminology'; +export { withProgramTerminologyContext } from './programTerminologyContext'; +export { useProgramT } from './useProgramT'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts new file mode 100644 index 0000000000..3e6a90aa18 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts @@ -0,0 +1,37 @@ +import type { TerminologyContext } from './applyCustomTerminology'; + +// Module-level stack of explicit program contexts. A stack (rather than a single +// value) correctly handles nested providers — e.g. a cross-program relationships +// widget inside a program-scoped page shell. +// +// Correctness note: this relies on React rendering being synchronous within a +// single tree. It is safe for non-concurrent render paths. If the app adopts +// React 18 concurrent features (startTransition, useDeferredValue) on paths +// that render cross-program widgets, revisit this mechanism. +const contextStack: Array = []; + +/** + * Runs `fn` with `context` as the active program terminology context. + * Any `i18n.t` calls made synchronously inside `fn` will use this context + * instead of the global Redux-derived context. + * + * Use this in non-React code (hooks, data builders, column factories) where + * you know the program that the translated strings are about — for example + * when building column definitions for a cross-program relationship widget. + */ +export const withProgramTerminologyContext = ( + context: TerminologyContext, + fn: () => T, +): T => { + contextStack.push(context); + try { + return fn(); + } finally { + contextStack.pop(); + } +}; + +/** Returns the innermost explicit context, or undefined if none is active. */ +export const getActiveProgramTerminologyContext = (): TerminologyContext | undefined => ( + contextStack.length > 0 ? contextStack[contextStack.length - 1] : undefined +); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts index 07c2156337..6b7e6b3fb7 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts @@ -1,32 +1,32 @@ import { getLocationQuery } from '../../../utils/routing'; +import { getActiveProgramTerminologyContext } from './programTerminologyContext'; import type { TerminologyContext } from './applyCustomTerminology'; type ReduxStore = { getState: () => unknown }; type DomainState = { + currentSelections?: { programId?: string }, enrollmentDomain?: { enrollment?: { program?: string }, }, }; export const resolveTerminologyContext = (store: ReduxStore): TerminologyContext => { - const query = getLocationQuery(); - - if (query.programId) { - return { - programId: query.programId, - stageId: query.stageId ?? query.programStageId, - }; - } + // Explicit context wins — set by withProgramTerminologyContext for cross-program widgets. + const explicit = getActiveProgramTerminologyContext(); + if (explicit !== undefined) return explicit; const state = (store.getState() ?? {}) as DomainState; + const programId = + state.currentSelections?.programId || + state.enrollmentDomain?.enrollment?.program; - if (query.enrollmentId || query.teiId) { - const enrollment = state.enrollmentDomain?.enrollment; - if (enrollment?.program) { - return { programId: enrollment.program }; - } - } + if (!programId) return {}; + + // stageId is not stored in Redux; read from the URL so that displayEventLabel + // overrides on tracker program stages apply on view/edit-event routes. + const query = getLocationQuery(); + const stageId = query.stageId ?? query.programStageId; - return {}; + return stageId ? { programId, stageId } : { programId }; }; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts new file mode 100644 index 0000000000..fcf0713664 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts @@ -0,0 +1,24 @@ +import { useCallback } from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withProgramTerminologyContext } from './programTerminologyContext'; + +/** + * Returns a `t` function that applies terminology for the given program/stage + * rather than the globally-selected program. + * + * Use this in React components that render data belonging to a program that + * differs from the one currently selected in the URL/Redux state — e.g. a + * relationships widget displaying events from a linked program. + * + * const t = useProgramT(relationship.program.id); + * return {t('New event')}; // uses the linked program's label + */ +export const useProgramT = ( + programId: string | undefined, + stageId?: string | undefined, +): ((key: string, options?: Record) => string) => + useCallback( + (key: string, options?: Record) => + withProgramTerminologyContext({ programId, stageId }, () => i18n.t(key, options as any)), + [programId, stageId], + ); diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts index bb12dba43c..4271797227 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts @@ -1,4 +1,3 @@ -import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; import { quickStore } from '../../IOUtils'; import { getContext } from '../../context'; @@ -27,19 +26,16 @@ const convert = (() => { })); })(); -const buildFieldsParam = (includePluralLabels: boolean): string => { - const labels = includePluralLabels ? 'displayName,displayTrackedEntityTypesLabel' : 'displayName'; - return `id,access,${labels},minAttributesRequiredToSearch,featureType,` + - 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + - 'translations[property,locale,value]'; -}; +const FIELDS = + 'id,access,displayName,minAttributesRequiredToSearch,featureType,' + + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + + 'translations[property,locale,value]'; export const storeTrackedEntityTypes = (ids: Array) => { - const includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); const query = { resource: 'trackedEntityTypes', params: { - fields: buildFieldsParam(includePluralLabels), + fields: FIELDS, filter: `id:in:[${ids.join(',')}]`, pageSize: ids.length, }, diff --git a/src/core_modules/capture-core/utils/routing/getLocationQuery.ts b/src/core_modules/capture-core/utils/routing/getLocationQuery.ts index 322d6ba38d..b3870d1b2d 100644 --- a/src/core_modules/capture-core/utils/routing/getLocationQuery.ts +++ b/src/core_modules/capture-core/utils/routing/getLocationQuery.ts @@ -1,21 +1,7 @@ -// Cached across calls — window.location.hash doesn't change mid-render, but -// this is called from hot paths (customLabels wrapper, epics) that would -// otherwise re-parse the URL and allocate a new object on every invocation. -let cachedHash: string | undefined; -let cachedQuery: Readonly> | undefined; - export const getLocationQuery = (): any => { - const hash = window.location.hash; - if (cachedQuery && hash === cachedHash) return cachedQuery; - const urlSearchParamString = hash.split('?')[1]; - const query = [...new URLSearchParams(urlSearchParamString).entries()].reduce>( - (accParams, [key, value]) => { - accParams[key] = value; - return accParams; - }, - {}, - ); - cachedHash = hash; - cachedQuery = Object.freeze(query); - return cachedQuery; + const urlSearchParamString = window.location.hash.split('?')[1]; + return [...new URLSearchParams(urlSearchParamString).entries()].reduce((accParams, [key, value]) => { + accParams[key] = value; + return accParams; + }, {}); }; diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 561b1762ee..6f3131edc2 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -14,7 +14,8 @@ declare module '@dhis2/d2-i18n' { const i18n: { t: (key: string, options?: any) => any; language: string; - // Add other methods as needed + use: (module: any) => typeof i18n; + options: Record; }; export default i18n; } From f3b34dc21f617af96c5da8ba15b48732d51881d9 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:52:58 +0000 Subject: [PATCH 024/118] fix: change approach --- i18n/en.pot | 4 +- .../useGroupedLinkedEntities.ts | 13 +-- .../customLabels/applyCustomTerminology.ts | 101 ------------------ .../bootstrapCustomTerminology.ts | 68 ------------ .../helpers/customLabels/customLabels.ts | 84 +++++++-------- .../metaData/helpers/customLabels/index.ts | 10 +- .../customLabels/programTerminologyContext.ts | 37 ------- .../customLabels/resolveTerminologyContext.ts | 32 ------ .../metaData/helpers/customLabels/useLabel.ts | 30 ++++++ .../helpers/customLabels/useProgramT.ts | 24 ----- .../capture-core/metaData/helpers/index.ts | 8 +- .../capture-core/metaData/index.ts | 8 +- src/store/getStore.ts | 3 - 13 files changed, 83 insertions(+), 339 deletions(-) delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts diff --git a/i18n/en.pot b/i18n/en.pot index 6c6b4d23cd..3a9cf58fe8 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T13:43:54.110Z\n" -"PO-Revision-Date: 2026-08-12T13:43:54.110Z\n" +"POT-Creation-Date: 2026-08-12T13:53:00.924Z\n" +"PO-Revision-Date: 2026-08-12T13:53:00.924Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts index 8a54b9b721..0da4a832ef 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts @@ -4,16 +4,11 @@ import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { errorCreator } from 'capture-core-utils'; import { dataElementTypes } from '../../../../metaData'; -import { withProgramTerminologyContext } from '../../../../metaData/helpers/customLabels'; import { RELATIONSHIP_ENTITIES } from '../constants'; import { convertClientToList, convertServerToClient } from '../../../../converters'; import type { GroupedLinkedEntities, LinkedEntityData } from './types'; import type { ApiLinkedEntity, InputRelationshipData, RelationshipTypes } from '../Types'; -const getConstraintTerminologyContext = (constraint: any) => ({ - programId: constraint.program?.id, - stageId: 'programStage' in constraint ? constraint.programStage.id : undefined, -}); const getFallbackFieldsByRelationshipEntity = { [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ @@ -230,13 +225,7 @@ export const useGroupedLinkedEntities = ( { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; - // Use the constraint's own program for terminology so that - // column headers (e.g. "Program stage name") reflect the - // linked program's labels, not the currently-selected program. - const columns = withProgramTerminologyContext( - getConstraintTerminologyContext(constraint), - () => getColumns(constraint), - ); + const columns = getColumns(constraint); const context = getContext(constraint, relationshipType.access, readOnly); accGroupedLinkedEntities.push({ diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts deleted file mode 100644 index 1c2ce47edb..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomTerminology.ts +++ /dev/null @@ -1,101 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { programCollection } from '../../../metaDataMemoryStores'; -import { CUSTOM_LABEL_FIELDS, resolveCustomLabel } from './customLabels'; -import type { CustomLabelKey, CustomLabels, CustomLabelField } from './customLabels'; - -export type TerminologyContext = { - programId?: string, - stageId?: string, -}; - -// Unicode "non-characters" (U+FDD0 / U+FDD1) — the Unicode standard reserves -// these to never be used for text, so they never collide with real content. -// The bootstrap wrapper brackets every interpolated value with these markers -// so we can skip them here and avoid rewriting server-supplied names that -// happen to contain a token word (e.g. a stage named "Birth event"). -export const INTERPOLATION_OPEN = '﷐'; -export const INTERPOLATION_CLOSE = '﷑'; -const INTERPOLATION_PATTERN = new RegExp(`${INTERPOLATION_OPEN}(.*?)${INTERPOLATION_CLOSE}`, 'g'); - -type TermEntry = { - key: CustomLabelKey, - plural: boolean, - english: string, -}; - -const TERM_ENTRIES: ReadonlyArray = ( - Object.entries(CUSTOM_LABEL_FIELDS) as ReadonlyArray<[CustomLabelKey, CustomLabelField]> -).flatMap(([key, forms]) => { - const out: TermEntry[] = []; - const addForm = (form: { english: string, aliases?: ReadonlyArray }, plural: boolean) => { - out.push({ key, plural, english: form.english }); - (form.aliases ?? []).forEach(alias => out.push({ key, plural, english: alias })); - }; - if (forms.plural) addForm(forms.plural, true); - if (forms.singular) addForm(forms.singular, false); - return out; -}).sort((a, b) => b.english.length - a.english.length); - -const escapeRegExp = (input: string): string => input.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); - -const COMBINED_PATTERN_SOURCE = String.raw`\b(${TERM_ENTRIES.map(entry => escapeRegExp(entry.english)).join('|')})\b`; -const COMBINED_PATTERN = new RegExp(COMBINED_PATTERN_SOURCE, 'gi'); -// Non-global variant used only for the wrapper fast-path — `.test` on a -// global regex is stateful (advances lastIndex), which we want to avoid. -const HAS_ANY_TOKEN_PATTERN = new RegExp(COMBINED_PATTERN_SOURCE, 'i'); - -export const hasCustomTerminologyTokens = (text: string): boolean => - typeof text === 'string' && HAS_ANY_TOKEN_PATTERN.test(text); - -const findEntry = (match: string): TermEntry | undefined => { - const lower = match.toLowerCase(); - return TERM_ENTRIES.find(entry => entry.english === lower); -}; - -const preserveCase = (match: string, replacement: string, locale: string): string => { - if (match.length > 1 && match === match.toLocaleUpperCase(locale)) { - return replacement.toLocaleUpperCase(locale); - } - const firstUpper = match.charAt(0).toLocaleUpperCase(locale); - if (match.startsWith(firstUpper)) { - return replacement.charAt(0).toLocaleUpperCase(locale) + replacement.slice(1); - } - return replacement; -}; - -const getLabelSources = ({ - programId, - stageId, -}: TerminologyContext): Array => { - const program = programId ? programCollection.get(programId) : undefined; - const stage = program && stageId ? program.getStage(stageId) : undefined; - return [stage?.customLabels, program?.customLabels]; -}; - -export const applyCustomTerminology = ( - translatedText: string, - context: TerminologyContext = {}, -): string => { - if (typeof translatedText !== 'string' || !translatedText) return translatedText; - const { programId, stageId } = context; - if (!programId && !stageId) return translatedText; - - const sources = getLabelSources(context); - const locale = i18n.language || 'en'; - - const substitute = (text: string): string => text.replace(COMBINED_PATTERN, (match) => { - const entry = findEntry(match); - if (!entry) return match; - const custom = resolveCustomLabel(sources, entry.key, { plural: entry.plural }); - if (!custom) return match; - return preserveCase(match, custom, locale); - }); - - // Split on sentinel-wrapped interpolation regions. String.split with a - // capturing group returns [outside, inside, outside, ...] — substitute - // only in the outside parts so server-supplied values pass through - // unchanged. If no sentinels are present, we get [translatedText] and - // just substitute the whole thing. - const parts = translatedText.split(INTERPOLATION_PATTERN); - return parts.map((part, i) => (i % 2 === 0 ? substitute(part) : part)).join(''); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts deleted file mode 100644 index 6d20034855..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/bootstrapCustomTerminology.ts +++ /dev/null @@ -1,68 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { - applyCustomTerminology, - hasCustomTerminologyTokens, - INTERPOLATION_OPEN, - INTERPOLATION_CLOSE, -} from './applyCustomTerminology'; -import { resolveTerminologyContext } from './resolveTerminologyContext'; - -type ReduxStore = { getState: () => unknown }; - -const I18N_CONTROL_KEYS = new Set([ - 'context', - 'count', - 'defaultValue', - 'fallbackLng', - 'interpolation', - 'joinArrays', - 'keySeparator', - 'lng', - 'lngs', - 'ns', - 'nsSeparator', - 'postProcess', - 'replace', - 'returnDetails', - 'returnObjects', - 'skipInterpolation', -]); - -const wrapInterpolationValues = (options?: Record): Record | undefined => { - if (!options || typeof options !== 'object') return options; - const wrapped: Record = {}; - for (const key of Object.keys(options)) { - const value = options[key]; - wrapped[key] = typeof value === 'string' && !I18N_CONTROL_KEYS.has(key) - ? `${INTERPOLATION_OPEN}${value}${INTERPOLATION_CLOSE}` - : value; - } - return wrapped; -}; - -let bootstrapped = false; - -export const bootstrapCustomTerminology = (store: ReduxStore) => { - if (bootstrapped) return; - bootstrapped = true; - - // Register terminology replacement as an i18next postProcessor plugin so it - // uses the framework's documented extension point rather than overwriting t. - i18n.use({ - type: 'postProcessor' as const, - name: 'customTerminology', - process(value: string): string { - if (!hasCustomTerminologyTokens(value)) return value; - return applyCustomTerminology(value, resolveTerminologyContext(store)); - }, - }); - // Enable the plugin globally — i18next reads this from options at call time. - (i18n.options as any).postProcess = 'customTerminology'; - - // Thin intercept solely to bracket interpolated values with sentinel markers - // before i18next performs interpolation. This prevents terminology replacement - // from rewriting server-supplied names (e.g. a stage called "Birth event"). - // No equivalent pre-interpolation hook exists in the i18next plugin API. - const originalT = i18n.t.bind(i18n); - i18n.t = (key: string, options?: any) => originalT(key, wrapInterpolationValues(options)); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 530add6dea..dd6c7c5d4c 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,63 +1,36 @@ -export type CustomLabelForm = { - field: string, - english: string, - aliases?: ReadonlyArray, -}; - -export type CustomLabelField = { - singular: CustomLabelForm, - plural?: CustomLabelForm, +type CustomLabelField = { + field?: string, + pluralField?: string, }; export const CUSTOM_LABEL_FIELDS = { - enrollment: { - singular: { field: 'displayEnrollmentLabel', english: 'enrollment' }, - plural: { field: 'displayEnrollmentsLabel', english: 'enrollments' }, - }, - event: { - singular: { field: 'displayEventLabel', english: 'event' }, - plural: { field: 'displayEventsLabel', english: 'events' }, - }, - note: { - singular: { field: 'displayNoteLabel', english: 'note' }, - }, - relationship: { - singular: { field: 'displayRelationshipLabel', english: 'relationship' }, - }, - attribute: { - singular: { field: 'displayTrackedEntityAttributeLabel', english: 'attribute' }, - }, - programStage: { - singular: { field: 'displayProgramStageLabel', english: 'program stage' }, - plural: { field: 'displayProgramStagesLabel', english: 'program stages' }, - }, - orgUnit: { - singular: { field: 'displayOrgUnitLabel', english: 'organisation unit' }, - plural: { field: 'displayOrgUnitLabel', english: 'organisation units' }, - }, - followUp: { - singular: { field: 'displayFollowUpLabel', english: 'follow-up' }, - }, + enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, + followUp: { field: 'displayFollowUpLabel' }, + orgUnit: { field: 'displayOrgUnitLabel' }, + relationship: { field: 'displayRelationshipLabel' }, + note: { field: 'displayNoteLabel' }, + attribute: { field: 'displayTrackedEntityAttributeLabel' }, + programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, + event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; export type CustomLabels = Record; export type LabelOptions = { plural?: boolean }; -const ALL_FIELDS: ReadonlyArray = Array.from( +const allFields: Array = Array.from( new Set( Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.singular.field, term.plural?.field]) + .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) .filter((field): field is string => Boolean(field)), ), ); -export const extractCustomLabels = (cached: Record): CustomLabels => { +export const extractCustomLabels = (cached: Record): CustomLabels => { const labels: CustomLabels = {}; - ALL_FIELDS.forEach((field) => { - const value = cached[field]; - if (typeof value === 'string' && value) { - labels[field] = value; + allFields.forEach((field) => { + if (cached[field]) { + labels[field] = cached[field]; } }); return labels; @@ -65,14 +38,29 @@ export const extractCustomLabels = (cached: Record): CustomLabe type LabelSource = CustomLabels | undefined | null; -export const resolveCustomLabel = ( +export const resolveLabel = ( sources: LabelSource | Array, key: CustomLabelKey, { plural = false }: LabelOptions = {}, ): string | undefined => { const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; const list = Array.isArray(sources) ? sources : [sources]; - const form = plural && term.plural ? term.plural : term.singular; - if (!form) return undefined; - return list.find(source => source?.[form.field])?.[form.field]; + const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); + + if (plural) { + return term.pluralField ? pick(term.pluralField) : pick(term.field); + } + return pick(term.field); }; + +type WithLabels = { customLabels?: CustomLabels } | undefined | null; + +export const getProgramLabel = (program: WithLabels, key: CustomLabelKey, options?: LabelOptions): string | undefined => + resolveLabel(program?.customLabels, key, options); + +export const getStageLabel = ( + stage: WithLabels, + program: WithLabels, + key: CustomLabelKey, + options?: LabelOptions, +): string | undefined => resolveLabel([stage?.customLabels, program?.customLabels], key, options); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index 499df058c3..0196f272ae 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -1,11 +1,9 @@ export { CUSTOM_LABEL_FIELDS, - resolveCustomLabel, + resolveLabel, extractCustomLabels, + getProgramLabel, + getStageLabel, } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; -export { applyCustomTerminology } from './applyCustomTerminology'; -export type { TerminologyContext } from './applyCustomTerminology'; -export { bootstrapCustomTerminology } from './bootstrapCustomTerminology'; -export { withProgramTerminologyContext } from './programTerminologyContext'; -export { useProgramT } from './useProgramT'; +export { useProgramLabel, useStageLabel } from './useLabel'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts deleted file mode 100644 index 3e6a90aa18..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/programTerminologyContext.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { TerminologyContext } from './applyCustomTerminology'; - -// Module-level stack of explicit program contexts. A stack (rather than a single -// value) correctly handles nested providers — e.g. a cross-program relationships -// widget inside a program-scoped page shell. -// -// Correctness note: this relies on React rendering being synchronous within a -// single tree. It is safe for non-concurrent render paths. If the app adopts -// React 18 concurrent features (startTransition, useDeferredValue) on paths -// that render cross-program widgets, revisit this mechanism. -const contextStack: Array = []; - -/** - * Runs `fn` with `context` as the active program terminology context. - * Any `i18n.t` calls made synchronously inside `fn` will use this context - * instead of the global Redux-derived context. - * - * Use this in non-React code (hooks, data builders, column factories) where - * you know the program that the translated strings are about — for example - * when building column definitions for a cross-program relationship widget. - */ -export const withProgramTerminologyContext = ( - context: TerminologyContext, - fn: () => T, -): T => { - contextStack.push(context); - try { - return fn(); - } finally { - contextStack.pop(); - } -}; - -/** Returns the innermost explicit context, or undefined if none is active. */ -export const getActiveProgramTerminologyContext = (): TerminologyContext | undefined => ( - contextStack.length > 0 ? contextStack[contextStack.length - 1] : undefined -); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts deleted file mode 100644 index 6b7e6b3fb7..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/resolveTerminologyContext.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getLocationQuery } from '../../../utils/routing'; -import { getActiveProgramTerminologyContext } from './programTerminologyContext'; -import type { TerminologyContext } from './applyCustomTerminology'; - -type ReduxStore = { getState: () => unknown }; - -type DomainState = { - currentSelections?: { programId?: string }, - enrollmentDomain?: { - enrollment?: { program?: string }, - }, -}; - -export const resolveTerminologyContext = (store: ReduxStore): TerminologyContext => { - // Explicit context wins — set by withProgramTerminologyContext for cross-program widgets. - const explicit = getActiveProgramTerminologyContext(); - if (explicit !== undefined) return explicit; - - const state = (store.getState() ?? {}) as DomainState; - const programId = - state.currentSelections?.programId || - state.enrollmentDomain?.enrollment?.program; - - if (!programId) return {}; - - // stageId is not stored in Redux; read from the URL so that displayEventLabel - // overrides on tracker program stages apply on view/edit-event routes. - const query = getLocationQuery(); - const stageId = query.stageId ?? query.programStageId; - - return stageId ? { programId, stageId } : { programId }; -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts new file mode 100644 index 0000000000..175b6e2dab --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -0,0 +1,30 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { programCollection } from '../../../metaDataMemoryStores'; +import { resolveLabel } from './customLabels'; +import type { CustomLabelKey, LabelOptions } from './customLabels'; + +type ProgramOptions = LabelOptions & { programId?: string }; +type StageOptions = LabelOptions & { programId?: string, stageId?: string }; + +export const useProgramLabel = (key: CustomLabelKey, { programId, plural }: ProgramOptions = {}): string | undefined => { + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const id = programId ?? currentProgramId; + return useMemo( + () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural }), + [id, key, plural], + ); +}; + +export const useStageLabel = ( + key: CustomLabelKey, + { programId, stageId, plural }: StageOptions = {}, +): string | undefined => { + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const pId = programId ?? currentProgramId; + return useMemo(() => { + const program = pId ? programCollection.get(pId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); + }, [pId, stageId, key, plural]); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts deleted file mode 100644 index fcf0713664..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useProgramT.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useCallback } from 'react'; -import i18n from '@dhis2/d2-i18n'; -import { withProgramTerminologyContext } from './programTerminologyContext'; - -/** - * Returns a `t` function that applies terminology for the given program/stage - * rather than the globally-selected program. - * - * Use this in React components that render data belonging to a program that - * differs from the one currently selected in the URL/Redux state — e.g. a - * relationships widget displaying events from a linked program. - * - * const t = useProgramT(relationship.program.id); - * return {t('New event')}; // uses the linked program's label - */ -export const useProgramT = ( - programId: string | undefined, - stageId?: string | undefined, -): ((key: string, options?: Record) => string) => - useCallback( - (key: string, options?: Record) => - withProgramTerminologyContext({ programId, stageId }, () => i18n.t(key, options as any)), - [programId, stageId], - ); diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index e5a22c7396..e4f2d5393b 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -19,9 +19,11 @@ export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { CUSTOM_LABEL_FIELDS, - resolveCustomLabel, + resolveLabel, extractCustomLabels, - applyCustomTerminology, - bootstrapCustomTerminology, + getProgramLabel, + getStageLabel, + useProgramLabel, + useStageLabel, } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 06898f4539..2b00a795b8 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -41,9 +41,11 @@ export { getProgramAndStageForEventProgram, getEventProgramEventAccess, CUSTOM_LABEL_FIELDS, - resolveCustomLabel, + resolveLabel, extractCustomLabels, - applyCustomTerminology, - bootstrapCustomTerminology, + getProgramLabel, + getStageLabel, + useProgramLabel, + useStageLabel, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/store/getStore.ts b/src/store/getStore.ts index ac3596982a..699b66dbe6 100644 --- a/src/store/getStore.ts +++ b/src/store/getStore.ts @@ -9,7 +9,6 @@ import { environments } from 'capture-core/constants/environments'; import { createOffline } from '@redux-offline/redux-offline'; import offlineConfig from '@redux-offline/redux-offline/lib/defaults'; import { getEffectReconciler, shouldDiscard, queueConfig } from 'capture-core/trackerOffline'; -import { bootstrapCustomTerminology } from 'capture-core/metaData/helpers/customLabels'; import { getPersistOptions } from './persist/persistOptionsGetter'; import { reducerDescriptions } from '../reducers/descriptions/trackerCapture.reducerDescriptions'; import { epics } from '../epics/trackerCapture.epics'; @@ -56,7 +55,5 @@ export async function getStore( epicMiddleware.run(epics); - bootstrapCustomTerminology(store); - return store; } From 2b92b9aaa2a91337b3b3cef6c03ea8b964fccd2d Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:01:34 +0000 Subject: [PATCH 025/118] fix: complete clean up after revert --- i18n/en.pot | 4 ++-- .../common/RelationshipsWidget/useGroupedLinkedEntities.ts | 1 - src/declarations.d.ts | 4 +--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3a9cf58fe8..de14654603 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T13:53:00.924Z\n" -"PO-Revision-Date: 2026-08-12T13:53:00.924Z\n" +"POT-Creation-Date: 2026-08-12T14:01:36.078Z\n" +"PO-Revision-Date: 2026-08-12T14:01:36.078Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts index 0da4a832ef..bcd69e09cb 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts @@ -9,7 +9,6 @@ import { convertClientToList, convertServerToClient } from '../../../../converte import type { GroupedLinkedEntities, LinkedEntityData } from './types'; import type { ApiLinkedEntity, InputRelationshipData, RelationshipTypes } from '../Types'; - const getFallbackFieldsByRelationshipEntity = { [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ id: 'trackedEntityTypeName', diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 6f3131edc2..1cc6a18172 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -13,9 +13,7 @@ declare module 'src/core_modules/*'; declare module '@dhis2/d2-i18n' { const i18n: { t: (key: string, options?: any) => any; - language: string; - use: (module: any) => typeof i18n; - options: Record; + // Add other methods as needed }; export default i18n; } From d66e4223d56270e35d1a539b7e8b556b1a454684 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:40:51 +0000 Subject: [PATCH 026/118] feat: update custom label handling and refactor label exports --- i18n/en.pot | 22 ++++++- .../helpers/customLabels/customLabels.ts | 36 +++-------- .../metaData/helpers/customLabels/index.ts | 10 +-- .../metaData/helpers/customLabels/useLabel.ts | 61 +++++++++++++------ .../capture-core/metaData/helpers/index.ts | 10 +-- .../capture-core/metaData/index.ts | 8 +-- 6 files changed, 78 insertions(+), 69 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index de14654603..a5f2413231 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T14:01:36.078Z\n" -"PO-Revision-Date: 2026-08-12T14:01:36.078Z\n" +"POT-Creation-Date: 2026-08-12T14:40:52.846Z\n" +"PO-Revision-Date: 2026-08-12T14:40:52.846Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2242,6 +2242,24 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" +msgid "program stage" +msgstr "program stage" + +msgid "note" +msgstr "note" + +msgid "relationship" +msgstr "relationship" + +msgid "attribute" +msgstr "attribute" + +msgid "organisation unit" +msgstr "organisation unit" + +msgid "follow-up" +msgstr "follow-up" + msgid "Program not found" msgstr "Program not found" diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index dd6c7c5d4c..04bd973732 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -5,13 +5,13 @@ type CustomLabelField = { export const CUSTOM_LABEL_FIELDS = { enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, - followUp: { field: 'displayFollowUpLabel' }, - orgUnit: { field: 'displayOrgUnitLabel' }, - relationship: { field: 'displayRelationshipLabel' }, + event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, + programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, note: { field: 'displayNoteLabel' }, + relationship: { field: 'displayRelationshipLabel' }, attribute: { field: 'displayTrackedEntityAttributeLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, + orgUnit: { field: 'displayOrgUnitLabel' }, + followUp: { field: 'displayFollowUpLabel' }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; @@ -29,9 +29,7 @@ const allFields: Array = Array.from( export const extractCustomLabels = (cached: Record): CustomLabels => { const labels: CustomLabels = {}; allFields.forEach((field) => { - if (cached[field]) { - labels[field] = cached[field]; - } + if (cached[field]) labels[field] = cached[field]; }); return labels; }; @@ -43,24 +41,8 @@ export const resolveLabel = ( key: CustomLabelKey, { plural = false }: LabelOptions = {}, ): string | undefined => { - const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; + const term = CUSTOM_LABEL_FIELDS[key] as CustomLabelField; const list = Array.isArray(sources) ? sources : [sources]; - const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); - - if (plural) { - return term.pluralField ? pick(term.pluralField) : pick(term.field); - } - return pick(term.field); + const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); + return plural && term.pluralField ? pick(term.pluralField) : pick(term.field); }; - -type WithLabels = { customLabels?: CustomLabels } | undefined | null; - -export const getProgramLabel = (program: WithLabels, key: CustomLabelKey, options?: LabelOptions): string | undefined => - resolveLabel(program?.customLabels, key, options); - -export const getStageLabel = ( - stage: WithLabels, - program: WithLabels, - key: CustomLabelKey, - options?: LabelOptions, -): string | undefined => resolveLabel([stage?.customLabels, program?.customLabels], key, options); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index 0196f272ae..08d6759b09 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -1,9 +1,3 @@ -export { - CUSTOM_LABEL_FIELDS, - resolveLabel, - extractCustomLabels, - getProgramLabel, - getStageLabel, -} from './customLabels'; +export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; -export { useProgramLabel, useStageLabel } from './useLabel'; +export { getTermLabel, useTermLabel } from './useLabel'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts index 175b6e2dab..9f0402fa65 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -1,30 +1,55 @@ +import i18n from '@dhis2/d2-i18n'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { programCollection } from '../../../metaDataMemoryStores'; import { resolveLabel } from './customLabels'; import type { CustomLabelKey, LabelOptions } from './customLabels'; -type ProgramOptions = LabelOptions & { programId?: string }; -type StageOptions = LabelOptions & { programId?: string, stageId?: string }; +const defaults: Record string> = { + enrollment: () => i18n.t('enrollment'), + event: () => i18n.t('event'), + programStage: () => i18n.t('program stage'), + note: () => i18n.t('note'), + relationship: () => i18n.t('relationship'), + attribute: () => i18n.t('attribute'), + orgUnit: () => i18n.t('organisation unit'), + followUp: () => i18n.t('follow-up'), +}; -export const useProgramLabel = (key: CustomLabelKey, { programId, plural }: ProgramOptions = {}): string | undefined => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const id = programId ?? currentProgramId; - return useMemo( - () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural }), - [id, key, plural], - ); +type TermLabelOptions = LabelOptions & { stageId?: string }; + +const resolve = ( + programId: string | undefined, + key: CustomLabelKey, + { stageId, plural }: TermLabelOptions, +): string => { + const program = programId ? programCollection.get(programId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }) + ?? defaults[key](); }; -export const useStageLabel = ( +/** + * Works anywhere — components, reducers, epics. + * Returns the custom label from the program (or stage), falling back to the + * translated default term. + */ +export const getTermLabel = ( + programId: string | undefined, + key: CustomLabelKey, + options: TermLabelOptions = {}, +): string => resolve(programId, key, options); + +/** + * React hook version — reads programId from Redux automatically. + * Pass programId explicitly to override (e.g. cross-program widgets). + */ +export const useTermLabel = ( key: CustomLabelKey, - { programId, stageId, plural }: StageOptions = {}, -): string | undefined => { + options: TermLabelOptions & { programId?: string } = {}, +): string => { + const { programId, stageId, plural } = options; const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const pId = programId ?? currentProgramId; - return useMemo(() => { - const program = pId ? programCollection.get(pId) : undefined; - const stage = program && stageId ? program.getStage(stageId) : undefined; - return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); - }, [pId, stageId, key, plural]); + const id = programId ?? currentProgramId; + return useMemo(() => resolve(id, key, { stageId, plural }), [id, key, stageId, plural]); }; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index e4f2d5393b..c46d3adf0e 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -17,13 +17,5 @@ export { getScopeInfo } from './getScopeInfo'; export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; -export { - CUSTOM_LABEL_FIELDS, - resolveLabel, - extractCustomLabels, - getProgramLabel, - getStageLabel, - useProgramLabel, - useStageLabel, -} from './customLabels'; +export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, useTermLabel } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 2b00a795b8..139fb995a4 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -41,11 +41,9 @@ export { getProgramAndStageForEventProgram, getEventProgramEventAccess, CUSTOM_LABEL_FIELDS, - resolveLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - useProgramLabel, - useStageLabel, + resolveLabel, + getTermLabel, + useTermLabel, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; From 762f6bd3e4f7ac9d0338c1bfbeaed853da7c6ee4 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:47:18 +0000 Subject: [PATCH 027/118] fix: correct capitalization in widget header and improve label resolution logic --- i18n/en.pot | 8 ++++---- .../WidgetStagesAndEvents.component.tsx | 2 +- .../metaData/helpers/customLabels/customLabels.ts | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a5f2413231..c735b7dc70 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T14:40:52.846Z\n" -"PO-Revision-Date: 2026-08-12T14:40:52.846Z\n" +"POT-Creation-Date: 2026-08-12T14:47:19.687Z\n" +"PO-Revision-Date: 2026-08-12T14:47:19.687Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1801,8 +1801,8 @@ msgstr "{{ scheduledEvents }} scheduled" msgid "No program stages found in this program" msgstr "No program stages found in this program" -msgid "Program stages and Events" -msgstr "Program stages and Events" +msgid "Program stages and events" +msgstr "Program stages and events" msgid "An error occurred while unlinking and deleting the event." msgstr "An error occurred while unlinking and deleting the event." diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index c17deb7e4c..13a96518fc 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -44,7 +44,7 @@ const WidgetStagesAndEventsPlain = ({ - {i18n.t('Program stages and Events')} + {i18n.t('Program stages and events')} {showWidgetBadge && (
(field ? list.find(s => s?.[field])?.[field] : undefined); - return plural && term.pluralField ? pick(term.pluralField) : pick(term.field); + if (plural && term.pluralField) { + return pick(term.pluralField) ?? pick(term.field); + } + return pick(term.field); }; From ee3d687ccbdb4401541d5474a958654c9dfed490 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Thu, 13 Aug 2026 09:56:14 +0000 Subject: [PATCH 028/118] fix: simplify label resolution logic by removing fallback for plural fields --- i18n/en.pot | 4 ++-- .../metaData/helpers/customLabels/customLabels.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index c735b7dc70..ea6ed29567 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-12T14:47:19.687Z\n" -"PO-Revision-Date: 2026-08-12T14:47:19.687Z\n" +"POT-Creation-Date: 2026-08-13T09:56:16.886Z\n" +"PO-Revision-Date: 2026-08-13T09:56:16.886Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 7396b753b5..bf60ed5c72 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -45,7 +45,7 @@ export const resolveLabel = ( const list = Array.isArray(sources) ? sources : [sources]; const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); if (plural && term.pluralField) { - return pick(term.pluralField) ?? pick(term.field); + return pick(term.pluralField); } return pick(term.field); }; From 8b7afbb63abf2839eb8c1e76ff6c216281c00972 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:18:20 +0000 Subject: [PATCH 029/118] feat: new translate function --- i18n/en.pot | 7 ++--- .../WidgetEnrollment.component.tsx | 15 +++++++--- .../ScheduleDate/ScheduleDate.component.tsx | 3 +- .../hooks/useStageLabels.ts | 9 ++++-- .../metaData/helpers/customLabels/index.ts | 2 ++ .../metaData/helpers/customLabels/tLabel.ts | 30 +++++++++++++++++++ .../metaData/helpers/customLabels/useLabel.ts | 9 ++++-- .../capture-core/metaData/helpers/index.ts | 10 ++++++- .../capture-core/metaData/index.ts | 2 ++ .../programStage/ProgramStageFactory.ts | 12 ++++++-- .../utils/capitalizeFirstLetter.ts | 11 +++++++ 11 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts create mode 100644 src/core_modules/capture-core/utils/capitalizeFirstLetter.ts diff --git a/i18n/en.pot b/i18n/en.pot index ea6ed29567..f0ec511df6 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-13T09:56:16.886Z\n" -"PO-Revision-Date: 2026-08-13T09:56:16.886Z\n" +"POT-Creation-Date: 2026-08-15T18:18:21.625Z\n" +"PO-Revision-Date: 2026-08-15T18:18:21.625Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1446,9 +1446,6 @@ msgstr "Enrollment date" msgid "Incident date" msgstr "Incident date" -msgid "Enrollment widget could not be loaded. Please try again later" -msgstr "Enrollment widget could not be loaded. Please try again later" - msgid "Follow-up" msgstr "Follow-up" diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 29b02e6023..3bff464153 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -17,7 +17,12 @@ import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; -import { dataElementTypes } from '../../metaData'; +import { + dataElementTypes, + useTermLabel, + tLabel, + capitalizeFirstLetter, +} from '../../metaData'; import { convertValue } from '../../converters/clientToView'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; @@ -82,6 +87,7 @@ const WidgetEnrollmentPlain = ({ onAccessLostFromTransfer, }: PlainProps & WithStyles) => { const { programWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); + const enrollmentLabel = useTermLabel('enrollment'); const enrollmentReadOnly = readOnlyMode || !programWriteAccess; const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); @@ -100,7 +106,7 @@ const WidgetEnrollmentPlain = ({ - {i18n.t('Enrollment')} + {capitalizeFirstLetter(enrollmentLabel)} {showWidgetBadge && (
setOpenStatus(false), [setOpenStatus])} open={open} > - {initError && ( + {true && (
- {i18n.t('Enrollment widget could not be loaded. Please try again later')} + {tLabel('{{enrollmentLabel}} widget could not be loaded. Please try again later', + { enrollmentLabel })}
)} {loading && } diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx index e27abe562d..ef5a063a4d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx @@ -11,6 +11,7 @@ import { } from 'capture-core/components/FormFields/New'; import { isValidDate, isValidPeriod } from 'capture-core/utils/validation/validators/form'; import { hasValue } from 'capture-core-utils/validators/form'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { systemSettingsStore } from '../../../metaDataMemoryStores'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { InfoBox } from '../InfoBox'; @@ -133,7 +134,7 @@ const ScheduleDatePlain = ({ /> :
- {displayDueDateLabel ?? i18n.t('Schedule date / Due date', { + {displayDueDateLabel ? capitalizeFirstLetter(displayDueDateLabel) : i18n.t('Schedule date / Due date', { interpolation: { escapeValue: false }, }, )} diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useStageLabels.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useStageLabels.ts index 2cdbd0a2ee..2955ca81c8 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useStageLabels.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useStageLabels.ts @@ -1,4 +1,5 @@ import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { getUserMetadataStorageController, USER_METADATA_STORES } from '../../../storageControllers'; import { useIndexedDBQuery } from '../../../utils/reactQueryHelpers'; @@ -23,8 +24,12 @@ export const useStageLabels = (programId: string, programStageId?: string) => { ); return { - scheduledLabel: data?.displayDueDateLabel ?? i18n.t('Scheduled date'), - occurredLabel: data?.displayExecutionDateLabel ?? i18n.t('Report date'), + scheduledLabel: data?.displayDueDateLabel + ? capitalizeFirstLetter(data.displayDueDateLabel) + : i18n.t('Scheduled date'), + occurredLabel: data?.displayExecutionDateLabel + ? capitalizeFirstLetter(data.displayExecutionDateLabel) + : i18n.t('Report date'), isLoading: isInitialLoading, error, }; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index 08d6759b09..9c9de5821a 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -1,3 +1,5 @@ export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; export { getTermLabel, useTermLabel } from './useLabel'; +export { tLabel } from './tLabel'; +export { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts new file mode 100644 index 0000000000..172b02b9c5 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts @@ -0,0 +1,30 @@ +import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; + +const getRawTranslation = (key: string): string => + (i18n as any).getResource((i18n as any).language, 'default', key) + ?? (i18n as any).getResource('en', 'default', key) + ?? key; + +const startsWithVar = (raw: string, varName: string): boolean => { + const trimmed = raw.trimStart(); + return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); +}; + +/** + * Drop-in replacement for i18n.t() when the string contains custom label variables. + * Automatically capitalizes a variable's value when it appears as the first word + * in the translated string — without requiring any changes to translation files. + */ +export const tLabel = (key: string, options: Record = {}): string => { + const raw = getRawTranslation(key); + const processedOptions = { ...options }; + + for (const [varName, value] of Object.entries(options)) { + if (typeof value === 'string' && startsWithVar(raw, varName)) { + processedOptions[varName] = capitalizeFirstLetter(value); + } + } + + return i18n.t(key, { ...processedOptions, interpolation: { escapeValue: false } }); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts index 9f0402fa65..dae529d57f 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -25,8 +25,8 @@ const resolve = ( ): string => { const program = programId ? programCollection.get(programId) : undefined; const stage = program && stageId ? program.getStage(stageId) : undefined; - return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }) - ?? defaults[key](); + const customLabel = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); + return customLabel ?? defaults[key](); }; /** @@ -51,5 +51,8 @@ export const useTermLabel = ( const { programId, stageId, plural } = options; const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); const id = programId ?? currentProgramId; - return useMemo(() => resolve(id, key, { stageId, plural }), [id, key, stageId, plural]); + return useMemo( + () => resolve(id, key, { stageId, plural }), + [id, key, stageId, plural], + ); }; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index c46d3adf0e..94a88e45ee 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -17,5 +17,13 @@ export { getScopeInfo } from './getScopeInfo'; export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; -export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, useTermLabel } from './customLabels'; +export { + CUSTOM_LABEL_FIELDS, + extractCustomLabels, + resolveLabel, + getTermLabel, + useTermLabel, + tLabel, + capitalizeFirstLetter, +} from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 139fb995a4..a098e321b4 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -45,5 +45,7 @@ export { resolveLabel, getTermLabel, useTermLabel, + tLabel, + capitalizeFirstLetter, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.ts index 6a886b8f72..8e10d40c4d 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.ts @@ -241,8 +241,16 @@ export class ProgramStageFactory { _form.description = cachedProgramStage.description; _form.featureType = ProgramStageFactory._getFeatureType(cachedProgramStage); _form.access = cachedProgramStage.access; - _form.addLabel({ id: 'occurredAt', label: cachedProgramStage.displayExecutionDateLabel || 'Report date' }); - _form.addLabel({ id: 'scheduledAt', label: cachedProgramStage.displayDueDateLabel || 'Scheduled date' }); + const executionLabel = cachedProgramStage.displayExecutionDateLabel; + const dueDateLabel = cachedProgramStage.displayDueDateLabel; + _form.addLabel({ + id: 'occurredAt', + label: executionLabel ? capitalizeFirstLetter(executionLabel) : 'Report date', + }); + _form.addLabel({ + id: 'scheduledAt', + label: dueDateLabel ? capitalizeFirstLetter(dueDateLabel) : 'Scheduled date', + }); _form.validationStrategy = cachedProgramStage.validationStrategy && camelCaseUppercaseString(cachedProgramStage.validationStrategy); diff --git a/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts b/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts new file mode 100644 index 0000000000..cf3370402a --- /dev/null +++ b/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts @@ -0,0 +1,11 @@ +import i18n from '@dhis2/d2-i18n'; + +export const capitalizeFirstLetter = (str: string): string => { + if (!str) return str; + const locale = (i18n as any).language ?? 'en'; + try { + return str.charAt(0).toLocaleUpperCase(locale) + str.slice(1); + } catch { + return str.charAt(0).toUpperCase() + str.slice(1); + } +}; From 458b7e72468f0dc3ca925057f4da324dfe422824 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:43:49 +0000 Subject: [PATCH 030/118] feat: add terminology for enrollment sigular and plural --- i18n/en.pot | 374 ++++++++++-------- .../hooks/useOriginLabel.ts | 27 +- .../EnrollmentBreadcrumb.tsx | 5 +- .../hooks/useWorkingListLabel.ts | 19 +- .../CardList/CardListButtons.component.tsx | 4 +- .../CardList/CardListItem.component.tsx | 4 +- .../EnrollmentDataEntry.component.tsx | 27 +- .../hooks/useDataEntrySections.ts | 11 +- .../EnrollmentRegistrationEntry.component.tsx | 8 +- .../TeiRegistrationEntry.component.tsx | 8 +- .../CompleteModal/CompleteModal.component.tsx | 184 +++++---- .../DataEntryWidgetOutput.container.ts | 7 +- .../EnrollmentPageDefault.container.tsx | 6 +- .../Enrollment/MissingMessage.component.tsx | 44 ++- .../Pages/Enrollment/TopBar.container.tsx | 5 +- .../TopBar/TopBar.component.tsx | 4 +- .../EnrollmentEditEvent/TopBar.container.tsx | 5 +- .../RegistrationDataEntry.component.tsx | 6 +- .../WidgetEventEditWrapper.tsx | 4 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 9 +- .../ReadOnlyBadge/ReadOnlyBadge.types.ts | 1 + .../WidgetBreakingTheGlass.component.tsx | 18 +- .../Actions/Actions.component.tsx | 4 +- .../CompleteModal/CompleteModal.component.tsx | 159 ++++---- .../Actions/Delete/Delete.component.tsx | 12 +- .../TransferModal/TransferModal.component.tsx | 7 +- .../WidgetEnrollment.component.tsx | 15 +- .../WidgetEnrollmentNote.component.tsx | 8 +- .../DataEntry/DataEntry.component.tsx | 101 ++--- .../RelationshipsWidget.component.tsx | 4 +- .../Setup/hooks/useFiltersOnly.ts | 12 +- .../Setup/hooks/useStaticTemplates.ts | 15 +- .../Actions/CompleteAction/CompleteAction.tsx | 66 ++-- .../hooks/useCompleteBulkEnrollments.ts | 9 +- .../DeleteEnrollmentsAction.tsx | 14 +- .../EnrollmentDeleteModal.tsx | 32 +- .../hooks/useDeleteEnrollments.ts | 4 +- .../DeleteTeiAction/DeleteTeiAction.tsx | 7 +- 38 files changed, 715 insertions(+), 534 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ea6ed29567..3192e743c8 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-13T09:56:16.886Z\n" -"PO-Revision-Date: 2026-08-13T09:56:16.886Z\n" +"POT-Creation-Date: 2026-08-15T18:43:50.743Z\n" +"PO-Revision-Date: 2026-08-15T18:43:50.743Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -42,20 +42,20 @@ msgstr "Bulk data entry" msgid "Program overview" msgstr "Program overview" -msgid "Active enrollments" -msgstr "Active enrollments" +msgid "Active {{enrollmentsLabel}}" +msgstr "Active {{enrollmentsLabel}}" -msgid "Completed enrollments" -msgstr "Completed enrollments" +msgid "Completed {{enrollmentsLabel}}" +msgstr "Completed {{enrollmentsLabel}}" -msgid "Cancelled enrollments" -msgstr "Cancelled enrollments" +msgid "Cancelled {{enrollmentsLabel}}" +msgstr "Cancelled {{enrollmentsLabel}}" msgid "Search" msgstr "Search" -msgid "Enrollment dashboard" -msgstr "Enrollment dashboard" +msgid "{{enrollmentLabel}} dashboard" +msgstr "{{enrollmentLabel}} dashboard" msgid "View event" msgstr "View event" @@ -81,8 +81,8 @@ msgstr "View {{programName}} dashboard" msgid "View dashboard" msgstr "View dashboard" -msgid "View active enrollment" -msgstr "View active enrollment" +msgid "View active {{enrollmentLabel}}" +msgstr "View active {{enrollmentLabel}}" msgid "Re-enroll in {{programName}}" msgstr "Re-enroll in {{programName}}" @@ -99,8 +99,8 @@ msgstr "Previously enrolled" msgid "Organisation unit" msgstr "Organisation unit" -msgid "Date of enrollment" -msgstr "Date of enrollment" +msgid "Date of {{enrollmentLabel}}" +msgstr "Date of {{enrollmentLabel}}" msgid "Last updated" msgstr "Last updated" @@ -141,9 +141,6 @@ msgstr "Area" msgid "Coordinate" msgstr "Coordinate" -msgid "Enrollment" -msgstr "Enrollment" - msgid "Complete event" msgstr "Complete event" @@ -175,8 +172,8 @@ msgstr "Please select {{categoryName}}" msgid "A date in the future is not allowed" msgstr "A date in the future is not allowed" -msgid "Saving a new enrollment in {{programName}} in {{orgUnitName}}." -msgstr "Saving a new enrollment in {{programName}} in {{orgUnitName}}." +msgid "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." +msgstr "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." msgid "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." msgstr "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." @@ -318,9 +315,6 @@ msgstr "Saving a {{trackedEntityName}}" msgid "without" msgstr "without" -msgid "enrollment" -msgstr "enrollment" - msgid "in" msgstr "in" @@ -333,25 +327,29 @@ msgstr "An error has occurred. See log for details" msgid "{{programStageName}} completed" msgstr "{{programStageName}} completed" -msgid "Would you like to complete the enrollment and all active events as well?" -msgstr "Would you like to complete the enrollment and all active events as well?" +msgid "" +"Would you like to complete the {{enrollmentLabel}} and all active events as " +"well?" +msgstr "" +"Would you like to complete the {{enrollmentLabel}} and all active events as " +"well?" msgid "{{count}} event in {{programStageName}}" msgid_plural "{{count}} event in {{programStageName}}" msgstr[0] "{{count}} event in {{programStageName}}" msgstr[1] "{{count}} events in {{programStageName}}" -msgid "Yes, complete enrollment and events" -msgstr "Yes, complete enrollment and events" +msgid "Yes, complete {{enrollmentLabel}} and events" +msgstr "Yes, complete {{enrollmentLabel}} and events" -msgid "Complete enrollment only" -msgstr "Complete enrollment only" +msgid "Complete {{enrollmentLabel}} only" +msgstr "Complete {{enrollmentLabel}} only" -msgid "Would you like to complete the enrollment?" -msgstr "Would you like to complete the enrollment?" +msgid "Would you like to complete the {{enrollmentLabel}}?" +msgstr "Would you like to complete the {{enrollmentLabel}}?" -msgid "Complete enrollment" -msgstr "Complete enrollment" +msgid "Complete {{enrollmentLabel}}" +msgstr "Complete {{enrollmentLabel}}" msgid "A duplicate exists (but there were some errors, see log for details" msgstr "A duplicate exists (but there were some errors, see log for details" @@ -424,11 +422,11 @@ msgstr "Some operations are still running. Please wait." msgid "Operations running" msgstr "Operations running" -msgid "No feedback for this enrollment yet" -msgstr "No feedback for this enrollment yet" +msgid "No feedback for this {{enrollmentLabel}} yet" +msgstr "No feedback for this {{enrollmentLabel}} yet" -msgid "No indicator output for this enrollment yet" -msgstr "No indicator output for this enrollment yet" +msgid "No indicator output for this {{enrollmentLabel}} yet" +msgstr "No indicator output for this {{enrollmentLabel}} yet" msgid "" "This event has unsaved changes. Leaving this page without saving will lose " @@ -730,27 +728,27 @@ msgstr "Make referral" msgid "No available program stages" msgstr "No available program stages" -msgid "Invalid enrollment id {{enrollmentId}}." -msgstr "Invalid enrollment id {{enrollmentId}}." +msgid "Invalid {{enrollmentLabel}} id {{enrollmentId}}." +msgstr "Invalid {{enrollmentLabel}} id {{enrollmentId}}." -msgid "Choose an enrollment to view the dashboard." -msgstr "Choose an enrollment to view the dashboard." +msgid "Choose an {{enrollmentLabel}} to view the dashboard." +msgstr "Choose an {{enrollmentLabel}} to view the dashboard." msgid "" -"Choose a program to add new or see existing enrollments for " +"Choose a program to add new or see existing {{enrollmentsLabel}} for " "{{teiDisplayName}}" msgstr "" -"Choose a program to add new or see existing enrollments for " +"Choose a program to add new or see existing {{enrollmentsLabel}} for " "{{teiDisplayName}}" msgid "{{programName}} has categories. Choose all categories to view dashboard." msgstr "{{programName}} has categories. Choose all categories to view dashboard." -msgid "There are no active enrollments." -msgstr "There are no active enrollments." +msgid "There are no active {{enrollmentsLabel}}." +msgstr "There are no active {{enrollmentsLabel}}." -msgid "Add new enrollment for {{teiDisplayName}} in this program." -msgstr "Add new enrollment for {{teiDisplayName}} in this program." +msgid "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." +msgstr "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." msgid "" "You do not have permissions to access to this program, registering unit or " @@ -767,10 +765,12 @@ msgstr "Enroll {{teiDisplayName}} in this program." msgid "" "{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " -"{{programName}}. Choose another program that allows {{tetName}} enrollment. " +"{{programName}}. Choose another program that allows {{tetName}} " +"{{enrollmentLabel}}. " msgstr "" "{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " -"{{programName}}. Choose another program that allows {{tetName}} enrollment. " +"{{programName}}. Choose another program that allows {{tetName}} " +"{{enrollmentLabel}}. " msgid "Enroll a new {{selectedTetName}} in this program." msgstr "Enroll a new {{selectedTetName}} in this program." @@ -903,8 +903,8 @@ msgstr "New" msgid "You can also choose a program from the top bar and create in that program" msgstr "You can also choose a program from the top bar and create in that program" -msgid "New Enrollment in program{{escape}} {{programName}}" -msgstr "New Enrollment in program{{escape}} {{programName}}" +msgid "New {{enrollmentLabel}} in program{{escape}} {{programName}}" +msgstr "New {{enrollmentLabel}} in program{{escape}} {{programName}}" msgid "Save {{trackedEntityTypeName}}" msgstr "Save {{trackedEntityTypeName}}" @@ -1050,8 +1050,8 @@ msgstr "Search form is missing. See log for details" msgid "Could not retrieve metadata. Please try again later." msgstr "Could not retrieve metadata. Please try again later." -msgid "The enrollment event data could not be found" -msgstr "The enrollment event data could not be found" +msgid "The {{enrollmentLabel}} event data could not be found" +msgstr "The {{enrollmentLabel}} event data could not be found" msgid "Loading" msgstr "Loading" @@ -1074,8 +1074,8 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "You only have view access to this enrollment" -msgstr "You only have view access to this enrollment" +msgid "You only have view access to this {{enrollmentLabel}}" +msgstr "You only have view access to this {{enrollmentLabel}}" msgid "You only have view access to this program" msgstr "You only have view access to this program" @@ -1306,31 +1306,31 @@ msgstr "No one is assigned to this event" msgid "Assign" msgstr "Assign" -msgid "Check for enrollments" -msgstr "Check for enrollments" +msgid "Check for {{enrollmentsLabel}}" +msgstr "Check for {{enrollmentsLabel}}" msgid "This program is protected" msgstr "This program is protected" msgid "" -"You must provide a reason to check for enrollments in this protected " -"program." +"You must provide a reason to check for {{enrollmentsLabel}} in this " +"protected program." msgstr "" -"You must provide a reason to check for enrollments in this protected " -"program." +"You must provide a reason to check for {{enrollmentsLabel}} in this " +"protected program." msgid "All activity will be logged." msgstr "All activity will be logged." -msgid "Reason to check for enrollments" -msgstr "Reason to check for enrollments" +msgid "Reason to check for {{enrollmentsLabel}}" +msgstr "Reason to check for {{enrollmentsLabel}}" msgid "" -"Describe the reason you are checking for enrollments in this protected " -"program" +"Describe the reason you are checking for {{enrollmentsLabel}} in this " +"protected program" msgstr "" -"Describe the reason you are checking for enrollments in this protected " -"program" +"Describe the reason you are checking for {{enrollmentsLabel}} in this " +"protected program" msgid "Unsaved changes" msgstr "Unsaved changes" @@ -1338,8 +1338,8 @@ msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" -msgid "Enrollment actions" -msgstr "Enrollment actions" +msgid "{{enrollmentLabel}} actions" +msgstr "{{enrollmentLabel}} actions" msgid "We are processing your request." msgstr "We are processing your request." @@ -1359,20 +1359,20 @@ msgstr "Mark as cancelled" msgid "Mark incomplete" msgstr "Mark incomplete" -msgid "You do not have access to delete this enrollment" -msgstr "You do not have access to delete this enrollment" +msgid "You do not have access to delete this {{enrollmentLabel}}" +msgstr "You do not have access to delete this {{enrollmentLabel}}" -msgid "Delete enrollment" -msgstr "Delete enrollment" +msgid "Delete {{enrollmentLabel}}" +msgstr "Delete {{enrollmentLabel}}" -msgid "Are you sure you want to delete this enrollment?" -msgstr "Are you sure you want to delete this enrollment?" +msgid "Are you sure you want to delete this {{enrollmentLabel}}?" +msgstr "Are you sure you want to delete this {{enrollmentLabel}}?" -msgid "This will permanently remove the current enrollment." -msgstr "This will permanently remove the current enrollment." +msgid "This will permanently remove the current {{enrollmentLabel}}." +msgstr "This will permanently remove the current {{enrollmentLabel}}." -msgid "Yes, delete enrollment." -msgstr "Yes, delete enrollment." +msgid "Yes, delete {{enrollmentLabel}}." +msgstr "Yes, delete {{enrollmentLabel}}." msgid "Remove mark for follow-up" msgstr "Remove mark for follow-up" @@ -1434,20 +1434,20 @@ msgid "Transfer Ownership" msgstr "Transfer Ownership" msgid "" -"Choose the organisation unit to which enrollment ownership should be " -"transferred." +"Choose the organisation unit to which {{enrollmentLabel}} ownership should " +"be transferred." msgstr "" -"Choose the organisation unit to which enrollment ownership should be " -"transferred." +"Choose the organisation unit to which {{enrollmentLabel}} ownership should " +"be transferred." -msgid "Enrollment date" -msgstr "Enrollment date" +msgid "{{enrollmentLabel}} date" +msgstr "{{enrollmentLabel}} date" msgid "Incident date" msgstr "Incident date" -msgid "Enrollment widget could not be loaded. Please try again later" -msgstr "Enrollment widget could not be loaded. Please try again later" +msgid "{{enrollmentLabel}} widget could not be loaded. Please try again later" +msgstr "{{enrollmentLabel}} widget could not be loaded. Please try again later" msgid "Follow-up" msgstr "Follow-up" @@ -1479,14 +1479,14 @@ msgstr "Saving to {{stageName}} for {{programName}}" msgid "Program or program stage is invalid" msgstr "Program or program stage is invalid" -msgid "Notes about this enrollment" -msgstr "Notes about this enrollment" +msgid "Notes about this {{enrollmentLabel}}" +msgstr "Notes about this {{enrollmentLabel}}" -msgid "Write a note about this enrollment" -msgstr "Write a note about this enrollment" +msgid "Write a note about this {{enrollmentLabel}}" +msgstr "Write a note about this {{enrollmentLabel}}" -msgid "This enrollment doesn't have any notes" -msgstr "This enrollment doesn't have any notes" +msgid "This {{enrollmentLabel}} doesn't have any notes" +msgstr "This {{enrollmentLabel}} doesn't have any notes" msgid "Error" msgstr "Error" @@ -1608,8 +1608,12 @@ msgstr "Save changes" msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." -msgid "Information about this enrollment can be edited in the Enrollment widget." -msgstr "Information about this enrollment can be edited in the Enrollment widget." +msgid "" +"Information about this {{enrollmentLabel}} can be edited in the " +"{{enrollmentLabel}} widget." +msgstr "" +"Information about this {{enrollmentLabel}} can be edited in the " +"{{enrollmentLabel}} widget." msgid "{{trackedEntityName}} profile" msgstr "{{trackedEntityName}} profile" @@ -1941,8 +1945,8 @@ msgstr "An error occurred while deleting the relationship." msgid "To open this relationship, please wait until saving is complete" msgstr "To open this relationship, please wait until saving is complete" -msgid "This enrollment doesn't have any relationships" -msgstr "This enrollment doesn't have any relationships" +msgid "This {{enrollmentLabel}} doesn't have any relationships" +msgstr "This {{enrollmentLabel}} doesn't have any relationships" msgid "Type" msgstr "Type" @@ -2013,8 +2017,8 @@ msgstr "Owner organisation unit" msgid "Registration Date" msgstr "Registration Date" -msgid "Enrollment status" -msgstr "Enrollment status" +msgid "{{enrollmentLabel}} status" +msgstr "{{enrollmentLabel}} status" msgid "Follow up" msgstr "Follow up" @@ -2022,110 +2026,123 @@ msgstr "Follow up" msgid "Choose a program stage to filter by {{label}}" msgstr "Choose a program stage to filter by {{label}}" -msgid "You do not have access to bulk complete enrollments" -msgstr "You do not have access to bulk complete enrollments" +msgid "You do not have access to bulk complete {{enrollmentsLabel}}" +msgstr "You do not have access to bulk complete {{enrollmentsLabel}}" msgid "" -"Some enrollments were completed successfully, but there was an error while " -"completing the rest. Please see the details below." +"Some {{enrollmentsLabel}} were completed successfully, but there was an " +"error while completing the rest. Please see the details below." msgstr "" -"Some enrollments were completed successfully, but there was an error while " -"completing the rest. Please see the details below." - -msgid "" -"There was an error while completing the enrollments. Please see the details " -"below." -msgstr "" -"There was an error while completing the enrollments. Please see the details " -"below." +"Some {{enrollmentsLabel}} were completed successfully, but there was an " +"error while completing the rest. Please see the details below." msgid "" -"An unexpected error occurred while fetching the enrollments. Please try " -"again." +"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " +"Please try again." msgstr "" -"An unexpected error occurred while fetching the enrollments. Please try " -"again." +"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " +"Please try again." -msgid "There are currently no active enrollments in the selection." -msgstr "There are currently no active enrollments in the selection." +msgid "There are currently no active {{enrollmentsLabel}} in the selection." +msgstr "There are currently no active {{enrollmentsLabel}} in the selection." -msgid "All enrollments are already completed or cancelled." -msgstr "All enrollments are already completed or cancelled." +msgid "All {{enrollmentsLabel}} are already completed or cancelled." +msgstr "All {{enrollmentsLabel}} are already completed or cancelled." -msgid "This action will complete {{count}} active enrollment in your selection." -msgid_plural "This action will complete {{count}} active enrollment in your selection." -msgstr[0] "This action will complete {{count}} active enrollment in your selection." -msgstr[1] "This action will complete {{count}} active enrollments in your selection." +msgid "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgid_plural "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgstr[0] "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgstr[1] "" +"This action will complete {{count}} active {{enrollmentsLabel}} in your " +"selection." -msgid "{{count}} enrollment already marked as completed will not be changed." -msgid_plural "{{count}} enrollment already marked as completed will not be changed." -msgstr[0] "{{count}} enrollment already marked as completed will not be changed." -msgstr[1] "{{count}} enrollments already marked as completed will not be changed." +msgid "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgid_plural "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgstr[0] "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgstr[1] "" +"{{count}} {{enrollmentsLabel}} already marked as completed will not be " +"changed." -msgid "Mark all events within enrollments as complete" -msgstr "Mark all events within enrollments as complete" +msgid "Mark all events within {{enrollmentsLabel}} as complete" +msgstr "Mark all events within {{enrollmentsLabel}} as complete" -msgid "Complete enrollments" -msgstr "Complete enrollments" +msgid "Complete {{enrollmentsLabel}}" +msgstr "Complete {{enrollmentsLabel}}" -msgid "Error completing enrollments" -msgstr "Error completing enrollments" +msgid "Error completing {{enrollmentsLabel}}" +msgstr "Error completing {{enrollmentsLabel}}" -msgid "No active enrollments to complete" -msgstr "No active enrollments to complete" +msgid "No active {{enrollmentsLabel}} to complete" +msgstr "No active {{enrollmentsLabel}} to complete" -msgid "Complete {{count}} enrollment" -msgid_plural "Complete {{count}} enrollment" -msgstr[0] "Complete {{count}} enrollment" -msgstr[1] "Complete {{count}} enrollments" +msgid "Complete {{count}} {{enrollmentLabel}}" +msgid_plural "Complete {{count}} {{enrollmentLabel}}" +msgstr[0] "Complete {{count}} {{enrollmentLabel}}" +msgstr[1] "Complete {{count}} {{enrollmentsLabel}}" -msgid "An error occurred when completing the enrollments" -msgstr "An error occurred when completing the enrollments" +msgid "An error occurred when completing the {{enrollmentsLabel}}" +msgstr "An error occurred when completing the {{enrollmentsLabel}}" -msgid "An unknown error occurred when completing enrollments" -msgstr "An unknown error occurred when completing enrollments" +msgid "An unknown error occurred when completing {{enrollmentsLabel}}" +msgstr "An unknown error occurred when completing {{enrollmentsLabel}}" -msgid "You do not have access to delete enrollments" -msgstr "You do not have access to delete enrollments" +msgid "You do not have access to delete {{enrollmentsLabel}}" +msgstr "You do not have access to delete {{enrollmentsLabel}}" -msgid "Delete enrollments" -msgstr "Delete enrollments" +msgid "Delete {{enrollmentsLabel}}" +msgstr "Delete {{enrollmentsLabel}}" -msgid "Delete selected enrollments" -msgstr "Delete selected enrollments" +msgid "Delete selected {{enrollmentsLabel}}" +msgstr "Delete selected {{enrollmentsLabel}}" -msgid "An error occurred while loading the selected enrollments. Please try again." -msgstr "An error occurred while loading the selected enrollments. Please try again." +msgid "" +"An error occurred while loading the selected {{enrollmentsLabel}}. Please " +"try again." +msgstr "" +"An error occurred while loading the selected {{enrollmentsLabel}}. Please " +"try again." msgid "" -"This action will permanently delete the selected enrollments, including all " -"associated data and events." +"This action will permanently delete the selected {{enrollmentsLabel}}, " +"including all associated data and events." msgstr "" -"This action will permanently delete the selected enrollments, including all " -"associated data and events." +"This action will permanently delete the selected {{enrollmentsLabel}}, " +"including all associated data and events." -msgid "Active enrollments ({{count}})" -msgid_plural "Active enrollments ({{count}})" -msgstr[0] "Active enrollments ({{count}})" -msgstr[1] "Active enrollments ({{count}})" +msgid "Active {{enrollmentsLabel}} ({{count}})" +msgid_plural "Active {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Active {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Active {{enrollmentsLabel}} ({{count}})" -msgid "Completed enrollments ({{count}})" -msgid_plural "Completed enrollments ({{count}})" -msgstr[0] "Completed enrollments ({{count}})" -msgstr[1] "Completed enrollments ({{count}})" +msgid "Completed {{enrollmentsLabel}} ({{count}})" +msgid_plural "Completed {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Completed {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Completed {{enrollmentsLabel}} ({{count}})" -msgid "Cancelled enrollments ({{count}})" -msgid_plural "Cancelled enrollments ({{count}})" -msgstr[0] "Cancelled enrollments ({{count}})" -msgstr[1] "Cancelled enrollments ({{count}})" +msgid "Cancelled {{enrollmentsLabel}} ({{count}})" +msgid_plural "Cancelled {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Cancelled {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Cancelled {{enrollmentsLabel}} ({{count}})" -msgid "Delete {{count}} enrollment" -msgid_plural "Delete {{count}} enrollment" -msgstr[0] "Delete {{count}} enrollment" -msgstr[1] "Delete {{count}} enrollments" +msgid "Delete {{count}} {{enrollmentLabel}}" +msgid_plural "Delete {{count}} {{enrollmentLabel}}" +msgstr[0] "Delete {{count}} {{enrollmentLabel}}" +msgstr[1] "Delete {{count}} {{enrollmentsLabel}}" -msgid "An error occurred when deleting enrollments" -msgstr "An error occurred when deleting enrollments" +msgid "An error occurred when deleting {{enrollmentsLabel}}" +msgstr "An error occurred when deleting {{enrollmentsLabel}}" msgid "Delete {{ trackedEntityName }} with all enrollments" msgstr "Delete {{ trackedEntityName }} with all enrollments" @@ -2135,8 +2152,12 @@ msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" msgstr[1] "Delete {{count}} {{ trackedEntityName }}" -msgid "Deleting records will also delete any associated enrollments and events." -msgstr "Deleting records will also delete any associated enrollments and events." +msgid "" +"Deleting records will also delete any associated {{enrollmentsLabel}} and " +"events." +msgstr "" +"Deleting records will also delete any associated {{enrollmentsLabel}} and " +"events." msgid "Are you sure you want to delete?" msgstr "Are you sure you want to delete?" @@ -2242,6 +2263,9 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" +msgid "enrollment" +msgstr "enrollment" + msgid "program stage" msgstr "program stage" diff --git a/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts index 7ec5c095b7..f3a088d899 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { breadcrumbsKeys } from '../BulkDataEntryBreadcrumb'; +import { useTermLabel } from '../../../../metaData'; type Props = { programId: string; @@ -10,30 +11,35 @@ type Props = { page: string; }; -const DefaultFilterLabels = { - default: i18n.t('Program overview'), - active: i18n.t('Active enrollments'), - complete: i18n.t('Completed enrollments'), - cancelled: i18n.t('Cancelled enrollments'), -}; - -const getWorkingListLabel = (selectedTemplate: any, selectedTemplateId: string) => { +const getWorkingListLabel = ( + selectedTemplate: any, + selectedTemplateId: string, + defaultFilterLabels: Record, +) => { if (selectedTemplate && !selectedTemplate.isDefault) { return selectedTemplate.name; } if (selectedTemplateId && !selectedTemplate) { - return DefaultFilterLabels[selectedTemplateId as keyof typeof DefaultFilterLabels]; + return defaultFilterLabels[selectedTemplateId as keyof typeof defaultFilterLabels]; } return i18n.t('Program overview'); }; export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) => { + const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); const workingListTemplates = useSelector(({ workingListsTemplates }: any) => workingListsTemplates?.teiList); const workingListProgramId = useSelector(({ workingListsContext }: any) => workingListsContext?.teiList?.programIdView); const { selectedTemplateId, loading: isLoadingTemplates, templates } = workingListTemplates ?? {}; const selectedTemplate = templates?.find(({ id }: any) => id === selectedTemplateId); const isSameProgram = workingListProgramId === programId; + const defaultFilterLabels = useMemo(() => ({ + default: i18n.t('Program overview'), + active: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + complete: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + cancelled: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + }), [enrollmentsLabel]); + const label = useMemo(() => { if (page === breadcrumbsKeys.SEARCH_PAGE) { return i18n.t('Search'); @@ -44,7 +50,7 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) } if (isSameProgram) { - return getWorkingListLabel(selectedTemplate, selectedTemplateId); + return getWorkingListLabel(selectedTemplate, selectedTemplateId, defaultFilterLabels); } if (!displayFrontPageList) { @@ -58,6 +64,7 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) selectedTemplate, selectedTemplateId, page, + defaultFilterLabels, ]); return { diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx index 2fc98e432e..2b4231e13c 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useState, ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { colors } from '@dhis2/ui'; +import { useTermLabel } from '../../../metaData'; import { DirectionalChevron } from '../../../utils/rtl'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; import { BreadcrumbItem } from '../common/BreadcrumbItem'; @@ -68,6 +69,7 @@ const BreadcrumbsPlain = ({ classes, }: Props) => { const [openWarning, setOpenWarning] = useState(null); + const enrollmentLabel = useTermLabel('enrollment', { programId }); const { label } = useWorkingListLabel({ programId, @@ -101,7 +103,7 @@ const BreadcrumbsPlain = ({ { key: pageKeys.OVERVIEW, onClick: () => handleNavigation(onBackToDashboard, pageKeys.OVERVIEW), - label: i18n.t('Enrollment dashboard'), + label: i18n.t('{{enrollmentLabel}} dashboard', { enrollmentLabel }), selected: page === pageKeys.OVERVIEW, condition: true, }, @@ -135,6 +137,7 @@ const BreadcrumbsPlain = ({ onBackToMainPage, onBackToDashboard, onBackToViewEvent, + enrollmentLabel, ]); return ( diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts index 130fa0efba..ab75045eb5 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts @@ -1,6 +1,7 @@ import i18n from '@dhis2/d2-i18n'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useTermLabel } from '../../../../metaData'; type Template = { id: string; @@ -22,17 +23,11 @@ const DefaultFilterKeys = { type DefaultFilterKey = typeof DefaultFilterKeys[keyof typeof DefaultFilterKeys]; -const DefaultFilterLabels: { [key in DefaultFilterKey]: string } = { - [DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'), - [DefaultFilterKeys.ACTIVE]: i18n.t('Active enrollments'), - [DefaultFilterKeys.COMPLETE]: i18n.t('Completed enrollments'), - [DefaultFilterKeys.CANCELLED]: i18n.t('Cancelled enrollments'), -}; - export const useWorkingListLabel = ({ programId, displayFrontPageList, }: Props) => { + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); const workingListTemplates = useSelector((state: any) => state.workingListsTemplates?.teiList); const workingListProgramId = useSelector((state: any) => state.workingListsContext?.teiList?.programIdView); @@ -41,6 +36,13 @@ export const useWorkingListLabel = ({ const selectedTemplate: Template | undefined = templates?.find(({ id }) => id === selectedTemplateId); const isSameProgram: boolean = workingListProgramId === programId; + const defaultFilterLabels: { [key in DefaultFilterKey]: string } = useMemo(() => ({ + [DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'), + [DefaultFilterKeys.ACTIVE]: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.COMPLETE]: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.CANCELLED]: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + }), [enrollmentsLabel]); + const label: string = useMemo(() => { if (isLoadingTemplates) return i18n.t('Loading...'); @@ -51,7 +53,7 @@ export const useWorkingListLabel = ({ if (selectedTemplateId && !selectedTemplate && DefaultFilterKeys[selectedTemplateId.toUpperCase() as keyof typeof DefaultFilterKeys]) { - return DefaultFilterLabels[selectedTemplateId as DefaultFilterKey]; + return defaultFilterLabels[selectedTemplateId as DefaultFilterKey]; } return i18n.t('Program overview'); @@ -66,6 +68,7 @@ export const useWorkingListLabel = ({ isSameProgram, selectedTemplate, selectedTemplateId, + defaultFilterLabels, ]); return { diff --git a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx index 24500322a4..59ec17be9c 100644 --- a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx @@ -8,6 +8,7 @@ import { availableCardListButtonState, enrollmentTypes } from './CardList.consta import { navigateToEnrollmentOverview, } from '../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; +import { useTermLabel } from '../../metaData'; type Props = { currentSearchScopeId?: string, @@ -79,6 +80,7 @@ const CardListButtons: FC = ({ }) => { const dispatch = useDispatch(); const navigationButtonsState: AvailableCardListButtonState = deriveNavigationButtonState(enrollmentType); + const enrollmentLabel = useTermLabel('enrollment'); const onHandleClick: ButtonEventHandler> = useCallback((_, event) => { event.stopPropagation(); @@ -115,7 +117,7 @@ const CardListButtons: FC = ({ { dataTest: 'view-active-enrollment-button', onClick: onHandleClick, - label: i18n.t('View active enrollment'), + label: i18n.t('View active {{enrollmentLabel}}', { enrollmentLabel }), hide: navigationButtonsState !== availableCardListButtonState.SHOW_VIEW_ACTIVE_ENROLLMENT_BUTTON, }, { diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx index 54d3ea1ec7..5177c463f0 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx @@ -17,6 +17,7 @@ import { getTrackerProgramThrowIfNotFound, OptionSet, type TrackerProgram, + useTermLabel, } from '../../metaData'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; @@ -152,6 +153,7 @@ const CardListItemIndex = ({ const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); const { orgUnitId, enrolledAt } = deriveEnrollmentOrgUnitIdAndDate(enrollments, enrollmentType, currentProgramId); const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(orgUnitId ?? null); + const enrollmentLabel = useTermLabel('enrollment', { programId: currentProgramId }); const program: TrackerProgram | undefined = enrollments.length ? deriveProgramFromEnrollment(enrollments, currentSearchScopeType) : undefined; @@ -221,7 +223,7 @@ const CardListItemIndex = ({ value={orgUnitName} /> diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx index 81802aa393..15dcfb016f 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx @@ -33,7 +33,7 @@ import { getIncidentDateValidatorContainer, } from './fieldValidators'; import { sectionKeysForEnrollmentDataEntry } from './constants/sectionKeys.const'; -import { type Enrollment, ProgramStage, RenderFoundation, getProgramThrowIfNotFound } from '../../../metaData'; +import { type Enrollment, ProgramStage, RenderFoundation, getProgramThrowIfNotFound, getTermLabel } from '../../../metaData'; import { EnrollmentWithFirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry'; import { getCategoryOptionsValidatorContainers, @@ -362,30 +362,31 @@ class FinalEnrollmentDataEntry extends React.Component { inMemoryFileStore.clear(); } - static dataEntrySectionDefinitions = { - [sectionKeysForEnrollmentDataEntry.ENROLLMENT]: { - placement: placements.TOP, - name: i18n.t('Enrollment'), - }, - [AOCsectionKey]: { - placement: placements.BOTTOM, - }, - }; - render() { - const { enrollmentMetadata, firstStageMetaData, relatedStageActionsOptions, ...passOnProps } = this.props; + const { enrollmentMetadata, firstStageMetaData, relatedStageActionsOptions, programId, ...passOnProps } = this.props; + + const dataEntrySections = { + [sectionKeysForEnrollmentDataEntry.ENROLLMENT]: { + placement: placements.TOP, + name: getTermLabel(programId, 'enrollment'), + }, + [AOCsectionKey]: { + placement: placements.BOTTOM, + }, + }; return ( firstStageMetaData ? ( ) : ( ) ); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.ts b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.ts index 3f2d62063a..32fd41353a 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.ts +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/hooks/useDataEntrySections.ts @@ -3,13 +3,15 @@ import i18n from '@dhis2/d2-i18n'; import { placements } from '../../../../DataEntry/constants/placements.const'; import { sectionKeysForFirstStageDataEntry } from '../EnrollmentWithFirstStageDataEntry.constants'; import { AOCsectionKey } from '../../../../DataEntryDhis2Helpers'; +import { useTermLabel } from '../../../../../metaData'; -export const useDataEntrySections = (stageName: string, beforeSectionId: string) => - useMemo( +export const useDataEntrySections = (stageName: string, beforeSectionId: string) => { + const enrollmentLabel = useTermLabel('enrollment'); + return useMemo( () => ({ [sectionKeysForFirstStageDataEntry.ENROLLMENT]: { placement: placements.TOP, - name: i18n.t('Enrollment'), + name: enrollmentLabel, }, [sectionKeysForFirstStageDataEntry.STAGE_BASIC_INFO]: { beforeSectionId, @@ -35,5 +37,6 @@ export const useDataEntrySections = (stageName: string, beforeSectionId: string) }), }, }), - [stageName, beforeSectionId], + [stageName, beforeSectionId, enrollmentLabel], ); +}; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx index 19b8892892..2216ad2a97 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx @@ -4,7 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { compose } from 'redux'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; -import { scopeTypes } from '../../../metaData'; +import { scopeTypes, useTermLabel } from '../../../metaData'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { EnrollmentDataEntry } from '../Enrollment'; import type { Props, PlainProps } from './EnrollmentRegistrationEntry.types'; @@ -26,10 +26,12 @@ const translatedTextWithStylesForProgram = ( trackedEntityName: string, programName: string, orgUnitName: string, + enrollmentLabel: string, teiId?: string, ) => ( teiId ? - {i18n.t('Saving a new enrollment in {{programName}} in {{orgUnitName}}.', { + {i18n.t('Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}.', { + enrollmentLabel, programName, orgUnitName, interpolation: { escapeValue: false }, @@ -65,6 +67,7 @@ const EnrollmentRegistrationEntryPlain = }: PlainProps & WithStyles) => { const [showWarning, setShowWarning] = useState(false); const { scopeType, trackedEntityName, programName } = useScopeInfo(selectedScopeId); + const enrollmentLabel = useTermLabel('enrollment'); const handleOnCancel = () => { if (!isUserInteractionInProgress) { @@ -122,6 +125,7 @@ const EnrollmentRegistrationEntryPlain = trackedEntityName.toLowerCase(), programName, orgUnit.name, + enrollmentLabel, teiId, )} diff --git a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.tsx index fff77883c9..722bd1b083 100644 --- a/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/TeiRegistrationEntry/TeiRegistrationEntry.component.tsx @@ -4,7 +4,7 @@ import { Button, spacers } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; -import { scopeTypes } from '../../../metaData'; +import { scopeTypes, useTermLabel } from '../../../metaData'; import { TrackedEntityInstanceDataEntry } from '../TrackedEntityInstance'; import { useCurrentOrgUnitId } from '../../../hooks/useCurrentOrgUnitId'; import { useOrgUnitNameWithAncestors } from '../../../metadataRetrieval/orgUnitName'; @@ -19,13 +19,14 @@ import { useMetadataForRegistrationForm } from '../common/TEIAndEnrollment/useMe const translatedTextWithStylesForTei = ( trackedEntityName: string, + enrollmentLabel: string, orgUnitName?: string, hideProgramSelectionMessage?: boolean, ) => (<> {i18n.t('Saving a {{trackedEntityName}}', { trackedEntityName, interpolation: { escapeValue: false } }) - } {i18n.t('without')} {i18n.t('enrollment')} + } {i18n.t('without')} {enrollmentLabel} {orgUnitName && <>{' '}{i18n.t('in')} {orgUnitName}}.{' '} {!hideProgramSelectionMessage && i18n.t('Enroll in a program by selecting a program from the top bar.')} ); @@ -59,6 +60,7 @@ const TeiRegistrationEntryPlain = const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const orgUnitId = useCurrentOrgUnitId(); const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(orgUnitId); + const enrollmentLabel = useTermLabel('enrollment'); const handleOnCancel = () => { if (!isUserInteractionInProgress) { @@ -111,7 +113,7 @@ const TeiRegistrationEntryPlain =
{translatedTextWithStylesForTei( - trackedEntityName.toLowerCase(), orgUnitName, hideProgramSelectionMessage, + trackedEntityName.toLowerCase(), enrollmentLabel, orgUnitName, hideProgramSelectionMessage, )} diff --git a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx index 08e3dd866a..db8fe7a829 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx @@ -2,6 +2,7 @@ import { Modal, ModalActions, ModalContent, ModalTitle, Button, ButtonStrip } fr import React from 'react'; import i18n from '@dhis2/d2-i18n'; import type { PlainProps, PlainPropsWithEvents } from './completeModal.types'; +import { useTermLabel } from '../../../../../../metaData'; export const CompleteEnrollmentAndEventsModalComponent = ({ programStageName, @@ -10,95 +11,104 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ onCancel, onCompleteEnrollmentAndEvents, onCompleteEnrollment, -}: PlainPropsWithEvents) => ( - - - {i18n.t('{{programStageName}} completed', { - programStageName, - interpolation: { escapeValue: false }, - })} - - -

{i18n.t('Would you like to complete the enrollment and all active events as well?')}

+}: PlainPropsWithEvents) => { + const enrollmentLabel = useTermLabel('enrollment'); + return ( + + + {i18n.t('{{programStageName}} completed', { + programStageName, + interpolation: { escapeValue: false }, + })} + + +

{i18n.t( + 'Would you like to complete the {{enrollmentLabel}} and all active events as well?', + { enrollmentLabel }, + )}

- {Object.keys(programStagesWithActiveEvents).length !== 0 && ( - <> - {i18n.t('The following events will be completed:')} - {Object.keys(programStagesWithActiveEvents).map((key) => { - const { count, name } = programStagesWithActiveEvents[key]; - return ( -
    - {i18n.t('{{count}} event in {{programStageName}}', { - count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', - programStageName: name, - interpolation: { escapeValue: false }, - })} -
- ); - })} - - )} + {Object.keys(programStagesWithActiveEvents).length !== 0 && ( + <> + {i18n.t('The following events will be completed:')} + {Object.keys(programStagesWithActiveEvents).map((key) => { + const { count, name } = programStagesWithActiveEvents[key]; + return ( +
    + {i18n.t('{{count}} event in {{programStageName}}', { + count, + defaultValue: '{{count}} event in {{programStageName}}', + defaultValue_plural: '{{count}} events in {{programStageName}}', + programStageName: name, + interpolation: { escapeValue: false }, + })} +
+ ); + })} + + )} - {Object.keys(programStagesWithoutAccess).length !== 0 && ( - <> - {i18n.t('The following events will not be completed due to lack of access:')} - {Object.keys(programStagesWithoutAccess).map((key) => { - const { count, name } = programStagesWithoutAccess[key]; + {Object.keys(programStagesWithoutAccess).length !== 0 && ( + <> + {i18n.t('The following events will not be completed due to lack of access:')} + {Object.keys(programStagesWithoutAccess).map((key) => { + const { count, name } = programStagesWithoutAccess[key]; - return ( -
    - {i18n.t('{{count}} event in {{programStageName}}', { - count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', - programStageName: name, - interpolation: { escapeValue: false }, - })} -
- ); - })} - - )} + return ( +
    + {i18n.t('{{count}} event in {{programStageName}}', { + count, + defaultValue: '{{count}} event in {{programStageName}}', + defaultValue_plural: '{{count}} events in {{programStageName}}', + programStageName: name, + interpolation: { escapeValue: false }, + })} +
+ ); + })} + + )} - - - - - - - -
-
-); + + + + + + + +
+
+ ); +}; -export const CompleteEnrollmentModalComponent = ({ programStageName, onCancel, onCompleteEnrollment }: PlainProps) => ( - - - {i18n.t('{{programStageName}} completed', { - programStageName, - interpolation: { escapeValue: false }, - })} - - -

{i18n.t('Would you like to complete the enrollment?')}

- - - - - - -
-
-); +export const CompleteEnrollmentModalComponent = ({ programStageName, onCancel, onCompleteEnrollment }: PlainProps) => { + const enrollmentLabel = useTermLabel('enrollment'); + return ( + + + {i18n.t('{{programStageName}} completed', { + programStageName, + interpolation: { escapeValue: false }, + })} + + +

{i18n.t('Would you like to complete the {{enrollmentLabel}}?', { enrollmentLabel })}

+ + + + + + +
+
+ ); +}; diff --git a/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts b/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts index bca310bb95..bbb9a81196 100644 --- a/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts +++ b/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts @@ -4,6 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { DataEntryWidgetOutputComponent } from './DataEntryWidgetOutput.component'; import { getDataEntryKey } from '../DataEntry/common/getDataEntryKey'; import { makeProgramRulesSelector } from './DataEntryWidgetOutput.selectors'; +import { getTermLabel } from '../../metaData'; type OwnProps = { dataEntryId: string; @@ -17,13 +18,15 @@ const makeMapStateToProps = () => { const { dataEntries } = state; const ready = !!dataEntries[dataEntryId]; const dataEntryKey = ready ? getDataEntryKey(dataEntryId, state.dataEntries[dataEntryId].itemId) : null; + const programId = state.currentSelections?.programId; + const enrollmentLabel = getTermLabel(programId, 'enrollment'); return { ready, dataEntryKey, programRules: programRulesSelector(state, { dataEntryId, selectedScopeId }), - feedbackEmptyText: i18n.t('No feedback for this enrollment yet'), - indicatorEmptyText: i18n.t('No indicator output for this enrollment yet'), + feedbackEmptyText: i18n.t('No feedback for this {{enrollmentLabel}} yet', { enrollmentLabel }), + indicatorEmptyText: i18n.t('No indicator output for this {{enrollmentLabel}} yet', { enrollmentLabel }), }; }; }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index bfa61fbf85..36b9c1de43 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx @@ -6,6 +6,7 @@ import { formatMomentEn } from 'capture-core-utils/date'; import { useDispatch, useSelector } from 'react-redux'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; +import { useTermLabel } from '../../../../metaData'; import { commitEnrollmentAndEvents, EnrollmentAccessProvider, @@ -53,6 +54,7 @@ export const EnrollmentPageDefault = () => { const { fromClientDate } = useTimeZoneConversion(); const { status: widgetEnrollmentStatus } = useSelector(({ widgetEnrollment }: any) => widgetEnrollment); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); + const enrollmentLabel = useTermLabel('enrollment', { programId }); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); const { onLinkedRecordClick } = useLinkedRecordClick(); const { @@ -233,8 +235,8 @@ export const EnrollmentPageDefault = () => { ruleEffects={ruleEffects} widgetEnrollmentStatus={widgetEnrollmentStatus} onAccessLostFromTransfer={onAccessLostFromTransfer} - feedbackEmptyText={i18n.t('No feedback for this enrollment yet')} - indicatorEmptyText={i18n.t('No indicator output for this enrollment yet')} + feedbackEmptyText={i18n.t('No feedback for this {{enrollmentLabel}} yet', { enrollmentLabel })} + indicatorEmptyText={i18n.t('No indicator output for this {{enrollmentLabel}} yet', { enrollmentLabel })} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index fa1043c8e2..800696f386 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -5,6 +5,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { useMissingCategoriesInProgramSelection } from '../../../hooks/useMissingCategoriesInProgramSelection'; import { scopeTypes } from '../../../metaData/helpers/constants'; +import { useTermLabel } from '../../../metaData'; import { enrollmentAccessLevels } from './EnrollmentPage.constants'; import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; @@ -151,17 +152,21 @@ const styles: Readonly = { }, }; -const EnrollmentSelectionMessage = ({ enrollmentId }: { enrollmentId?: string }) => ( - - {enrollmentId ? - i18n.t('Invalid enrollment id {{enrollmentId}}.', { - enrollmentId, - interpolation: { escapeValue: false }, - }) : - i18n.t('Choose an enrollment to view the dashboard.') - } - -); +const EnrollmentSelectionMessage = ({ enrollmentId }: { enrollmentId?: string }) => { + const enrollmentLabel = useTermLabel('enrollment'); + return ( + + {enrollmentId ? + i18n.t('Invalid {{enrollmentLabel}} id {{enrollmentId}}.', { + enrollmentLabel, + enrollmentId, + interpolation: { escapeValue: false }, + }) : + i18n.t('Choose an {{enrollmentLabel}} to view the dashboard.', { enrollmentLabel }) + } + + ); +}; type PlainProps = Record; @@ -180,6 +185,8 @@ const MissingMessagePlain = ({ const { resetTeiId } = useResetTeiId(); const { teiDisplayName, tetId } = useSelector(({ enrollmentPage }: any) => enrollmentPage); const { programId, teiId, enrollmentId } = useLocationQuery(); + const enrollmentLabel = useTermLabel('enrollment'); + const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); const { trackedEntityName: tetName } = useScopeInfo(tetId); const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId); @@ -188,8 +195,8 @@ const MissingMessagePlain = ({ { missingStatus === missingStatuses.MISSING_PROGRAM_SELECTION && - {i18n.t('Choose a program to add new or see existing enrollments for {{teiDisplayName}}', { - teiDisplayName, interpolation: { escapeValue: false }, + {i18n.t('Choose a program to add new or see existing {{enrollmentsLabel}} for {{teiDisplayName}}', { + enrollmentsLabel, teiDisplayName, interpolation: { escapeValue: false }, })} } @@ -213,13 +220,16 @@ const MissingMessagePlain = ({ missingStatus === missingStatuses.MISSING_ENROLLMENT_SELECTION_ADD_NEW &&
- {i18n.t('There are no active enrollments.')} + {i18n.t('There are no active {{enrollmentsLabel}}.', { enrollmentsLabel })}
- {i18n.t('Add new enrollment for {{teiDisplayName}} in this program.', { teiDisplayName })} + {i18n.t( + 'Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program.', + { enrollmentLabel, teiDisplayName }, + )}
@@ -271,8 +281,8 @@ const MissingMessagePlain = ({
{/* eslint-disable-next-line max-len */} - {i18n.t('{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the {{programName}}. Choose another program that allows {{tetName}} enrollment. ', { - teiDisplayName, programName, tetName, interpolation: { escapeValue: false }, + {i18n.t('{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the {{programName}}. Choose another program that allows {{tetName}} {{enrollmentLabel}}. ', { + teiDisplayName, programName, tetName, enrollmentLabel, interpolation: { escapeValue: false }, })}
{ + const enrollmentLabel = useTermLabel('enrollment', { programId }); const { setProgramIdAndResetEnrollmentContext } = useSetProgramId(); const { setOrgUnitId } = useSetOrgUnitId(); const { setEnrollmentId } = useSetEnrollmentId(); @@ -73,7 +74,7 @@ export const TopBar = ({ onSelect={id => setEnrollmentId({ enrollmentId: id })} options={enrollmentsAsOptions} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} /> ) : <>} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx index b79e06c752..e830669970 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx @@ -1,5 +1,6 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { useTermLabel } from '../../../../metaData'; import { ScopeSelector, SingleLockedSelect, useReset } from '../../../ScopeSelector'; import { TopBarActions } from '../../../TopBarActions'; import type { Props } from './topBar.types'; @@ -24,6 +25,7 @@ export const EnrollmentAddEventTopBar = ({ enrollmentsAsOptions, }: Props) => { const { reset } = useReset(); + const enrollmentLabel = useTermLabel('enrollment', { programId }); return ( onResetEnrollmentId()} options={enrollmentsAsOptions || []} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} isUserInteractionInProgress={userInteractionInProgress} /> {stageName && ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx index dc2357d1a3..cf7c2d9d17 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx @@ -1,7 +1,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { dataEntryKeys } from 'capture-core/constants'; -import type { ProgramStage } from '../../../metaData'; +import { useTermLabel, type ProgramStage } from '../../../metaData'; import { pageStatuses } from './EnrollmentEditEventPage.constants'; import { ScopeSelector, @@ -47,6 +47,7 @@ export const TopBar = ({ isUserInteractionInProgress, }: Props) => { const { setOrgUnitId } = useSetOrgUnitId(); + const enrollmentLabel = useTermLabel('enrollment', { programId: programId ?? undefined }); const { resetProgramIdAndEnrollmentContext } = useResetProgramId(); const { resetOrgUnitId } = useResetOrgUnitId(); @@ -87,7 +88,7 @@ export const TopBar = ({ onClear={() => resetEnrollmentId('enrollment', { programId: programId ?? undefined, teiId })} options={enrollmentsAsOptions} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} isUserInteractionInProgress={isUserInteractionInProgress} /> { let url; @@ -198,7 +199,8 @@ const RegistrationDataEntryPlain = ({
{ - teiId ? i18n.t('New Enrollment in program{{escape}} {{programName}}', { + teiId ? i18n.t('New {{enrollmentLabel}} in program{{escape}} {{programName}}', { + enrollmentLabel, escape: ':', programName, interpolation: { escapeValue: false }, diff --git a/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.tsx b/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.tsx index 986655fa08..0862f82a0c 100644 --- a/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.tsx +++ b/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.tsx @@ -5,6 +5,7 @@ import { IncompleteSelectionsMessage } from '../../../IncompleteSelectionsMessag import { WidgetEventEdit } from '../../../WidgetEventEdit'; import type { Props } from '../../../WidgetEventEdit/widgetEventEdit.types'; import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramStage/useMetadataForProgramStage'; +import { useTermLabel } from '../../../../metaData'; type WidgetProps = { pageStatus: string; @@ -22,6 +23,7 @@ export const WidgetEventEditWrapper = ({ pageStatus, ...passOnProps }: WidgetPro isLoading, isError, } = useMetadataForProgramStage({ programId, stageId }); + const enrollmentLabel = useTermLabel('enrollment', { programId }); if (pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED) { return ( @@ -33,7 +35,7 @@ export const WidgetEventEditWrapper = ({ pageStatus, ...passOnProps }: WidgetPro if (pageStatus === pageStatuses.MISSING_DATA) { return ( - {i18n.t('The enrollment event data could not be found')} + {i18n.t('The {{enrollmentLabel}} event data could not be found', { enrollmentLabel })} ); } diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index ffffb97b53..04e50ae1c4 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -4,6 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; import type { Props, Access, ReadOnlyMessageInput } from './ReadOnlyBadge.types'; +import { useTermLabel } from '../../metaData'; const styles = { label: { @@ -11,7 +12,8 @@ const styles = { }, } as const; -const getEnrollmentMessage = (): string => i18n.t('You only have view access to this enrollment'); +const getEnrollmentMessage = (enrollmentLabel: string): string => + i18n.t('You only have view access to this {{enrollmentLabel}}', { enrollmentLabel }); const getProgramMessage = (): string => i18n.t('You only have view access to this program'); @@ -40,9 +42,10 @@ const getReadOnlyMessage = ({ canEditCompletedEvent, withinCompleteEventsExpiry, trackedEntityInactive, + enrollmentLabel, }: ReadOnlyMessageInput): string => { if (trackedEntityInactive) return getDeactivatedMessage(trackedEntityName); - if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(); + if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(enrollmentLabel); if (!access.program) return getProgramMessage(); if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); if (!access.programStage) return getProgramStageMessage(multipleStages); @@ -65,6 +68,7 @@ const ReadOnlyBadgePlain = ({ inlineLabel = false, classes, }: Props & WithStyles) => { + const enrollmentLabel = useTermLabel('enrollment'); const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, @@ -78,6 +82,7 @@ const ReadOnlyBadgePlain = ({ canEditCompletedEvent, withinCompleteEventsExpiry, trackedEntityInactive, + enrollmentLabel, }); if (!message) return null; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts index a7ada9cb08..d1f94a4f4b 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts @@ -25,4 +25,5 @@ export type ReadOnlyMessageInput = { canEditCompletedEvent: boolean; withinCompleteEventsExpiry: boolean; trackedEntityInactive: boolean; + enrollmentLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx index 494b3c5ba1..63da23437e 100644 --- a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx +++ b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx @@ -10,6 +10,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { PlainProps } from './WidgetBreakingTheGlass.types'; import { Widget } from '../Widget'; +import { useTermLabel } from '../../metaData'; const styles: Readonly = ({ typography }: any) => ({ title: { @@ -35,6 +36,7 @@ const WidgetBreakingTheGlassPlain = ({ setReason(value); }, [setReason]); const disabled = useMemo(() => reason.length === 0, [reason]); + const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); return (
@@ -45,19 +47,25 @@ const WidgetBreakingTheGlassPlain = ({ >
- {i18n.t('Check for enrollments')} + {i18n.t('Check for {{enrollmentsLabel}}', { enrollmentsLabel })}

- {i18n.t('You must provide a reason to check for enrollments in this protected program.')} + {i18n.t( + 'You must provide a reason to check for {{enrollmentsLabel}} in this protected program.', + { enrollmentsLabel }, + )} {' '} {i18n.t('All activity will be logged.')}
- - - - - - -); + + + + + + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/Delete.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/Delete.component.tsx index f6dde67b74..00c1b73a70 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/Delete.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/Delete.component.tsx @@ -12,11 +12,13 @@ import { import i18n from '@dhis2/d2-i18n'; import type { Props } from './delete.types'; import { ConditionalTooltip } from '../../../Tooltips/ConditionalTooltip/'; +import { useTermLabel } from '../../../../metaData'; export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Props) => { const [toggle, setToggle] = useState(false); const disabled = !canCascadeDeleteEnrollment; - const tooltipContent = i18n.t('You do not have access to delete this enrollment'); + const enrollmentLabel = useTermLabel('enrollment'); + const tooltipContent = i18n.t('You do not have access to delete this {{enrollmentLabel}}', { enrollmentLabel }); return ( @@ -37,11 +39,11 @@ export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Pro onClose={() => setToggle(false)} dataTest="widget-enrollment-actions-modal" > - {i18n.t('Delete enrollment')} + {i18n.t('Delete {{enrollmentLabel}}', { enrollmentLabel })} - {i18n.t('Are you sure you want to delete this enrollment?')} + {i18n.t('Are you sure you want to delete this {{enrollmentLabel}}?', { enrollmentLabel })} {' '} - {i18n.t('This will permanently remove the current enrollment.')} + {i18n.t('This will permanently remove the current {{enrollmentLabel}}.', { enrollmentLabel })} @@ -52,7 +54,7 @@ export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Pro destructive onClick={() => onDelete(enrollment)} > - {i18n.t('Yes, delete enrollment.')} + {i18n.t('Yes, delete {{enrollmentLabel}}.', { enrollmentLabel })} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx index 0ed8c66bc4..6952202814 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx @@ -12,6 +12,7 @@ import type { TransferModalProps } from './TransferModal.types'; import { OrgUnitField } from './OrgUnitField'; import { useTransferValidation } from './hooks/useTransferValidation'; import { InfoBoxes } from './InfoBoxes'; +import { useTermLabel } from '../../../metaData'; export const TransferModal = ({ enrollment, @@ -20,6 +21,7 @@ export const TransferModal = ({ onUpdateOwnership, isTransferLoading, }: TransferModalProps) => { + const enrollmentLabel = useTermLabel('enrollment', { programId: enrollment.program }); const { selectedOrgUnit, handleOrgUnitChange, @@ -51,7 +53,10 @@ export const TransferModal = ({
- {i18n.t('Choose the organisation unit to which enrollment ownership should be transferred.')} + {i18n.t( + 'Choose the organisation unit to which {{enrollmentLabel}} ownership should be transferred.', + { enrollmentLabel }, + )}
(geometryType === 'Point' ? dataElementTypes.COORDINATE : dataElementTypes.POLYGON); -const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); +const getEnrollmentDateLabel = (program, enrollmentLabel: string) => + program.displayEnrollmentDateLabel ?? i18n.t('{{enrollmentLabel}} date', { enrollmentLabel }); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); const WidgetEnrollmentPlain = ({ @@ -82,6 +83,7 @@ const WidgetEnrollmentPlain = ({ onAccessLostFromTransfer, }: PlainProps & WithStyles) => { const { programWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); + const enrollmentLabel = useTermLabel('enrollment'); const enrollmentReadOnly = readOnlyMode || !programWriteAccess; const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); @@ -100,7 +102,7 @@ const WidgetEnrollmentPlain = ({ - {i18n.t('Enrollment')} + {enrollmentLabel} {showWidgetBadge && (
{initError && (
- {i18n.t('Enrollment widget could not be loaded. Please try again later')} + {i18n.t( + '{{enrollmentLabel}} widget could not be loaded. Please try again later', + { enrollmentLabel }, + )}
)} {loading && } @@ -135,7 +140,7 @@ const WidgetEnrollmentPlain = ({
{ const dispatch = useDispatch(); @@ -17,6 +18,7 @@ export const WidgetEnrollmentNote = () => { trackedEntityTypeName, showWidgetBadge, } = useEnrollmentAccessContext(); + const enrollmentLabel = useTermLabel('enrollment'); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); @@ -25,9 +27,9 @@ export const WidgetEnrollmentNote = () => { return (
( - - - {modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( - - )} - {(modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( - - )} - - } - > - {i18n.t( - 'Change information about this {{trackedEntityName}} here.', - { trackedEntityName, interpolation: { escapeValue: false } }, - )} - {' '} - {i18n.t('Information about this enrollment can be edited in the Enrollment widget.')} - - - -); + {modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( + + )} + {(modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( + + )} + + } + > + {i18n.t( + 'Change information about this {{trackedEntityName}} here.', + { trackedEntityName, interpolation: { escapeValue: false } }, + )} + {' '} + {i18n.t( + 'Information about this {{enrollmentLabel}} can be edited in the {{enrollmentLabel}} widget.', + { enrollmentLabel }, + )} + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx index 7948fbb2ae..33c939c40d 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx @@ -10,6 +10,7 @@ import { LinkedEntitiesViewer } from './LinkedEntitiesViewer.component'; import type { Props } from './relationshipsWidget.types'; import { LoadingMaskElementCenter } from '../../../LoadingMasks'; import { useDeleteRelationship } from './DeleteRelationship/useDeleteRelationship'; +import { useTermLabel } from '../../../../metaData'; const styles = { header: {}, @@ -38,6 +39,7 @@ const RelationshipsWidgetPlain = ({ const [open, setOpenStatus] = useState(true); const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships, readOnly); const { onDeleteRelationship } = useDeleteRelationship({ sourceId }); + const enrollmentLabel = useTermLabel('enrollment'); if (isLoading) { return ( @@ -92,7 +94,7 @@ const RelationshipsWidgetPlain = ({ } {(relationships?.length ?? 0) === 0 && (
- {i18n.t("This enrollment doesn't have any relationships")} + {i18n.t("This {{enrollmentLabel}} doesn't have any relationships", { enrollmentLabel })}
)} {children} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts index 41b1b20421..b57b58774a 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts @@ -1,21 +1,22 @@ import { useMemo } from 'react'; import { featureAvailable, FEATURES } from 'capture-core-utils'; import i18n from '@dhis2/d2-i18n'; -import { dataElementTypes, type TrackerProgram } from '../../../../../metaData'; +import { dataElementTypes, type TrackerProgram, useTermLabel } from '../../../../../metaData'; import { MAIN_FILTERS } from '../../constants'; export const useFiltersOnly = ( { enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, programStageId?: string, -) => - useMemo(() => { +) => { + const enrollmentLabel = useTermLabel('enrollment'); + return useMemo(() => { const enableUserAssignment = !programStageId && Array.from(stages.values()).find((stage: any) => stage.enableUserAssignment); return [ { id: MAIN_FILTERS.PROGRAM_STATUS, type: dataElementTypes.TEXT, - header: i18n.t('Enrollment status'), + header: i18n.t('{{enrollmentLabel}} status', { enrollmentLabel }), options: [ { text: i18n.t('Active'), value: 'ACTIVE' }, { text: i18n.t('Completed'), value: 'COMPLETED' }, @@ -99,4 +100,5 @@ export const useFiltersOnly = ( ] : []), ]; - }, [enrollmentDateLabel, incidentDateLabel, showIncidentDate, stages, programStageId]); + }, [enrollmentDateLabel, incidentDateLabel, showIncidentDate, stages, programStageId, enrollmentLabel]); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts index 552a9a7653..bd86f2269b 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts @@ -1,9 +1,11 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import type { WorkingListTemplate } from '../../../WorkingListsBase'; +import { useTermLabel } from '../../../../../metaData'; -export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => - useMemo( +export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => { + const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); + return useMemo( () => [ defaultAlteredTemplate || { id: defaultTemplateId, @@ -18,7 +20,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'active', - name: i18n.t('Active enrollments'), + name: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), order: 1, access: { update: false, @@ -32,7 +34,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'complete', - name: i18n.t('Completed enrollments'), + name: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), order: 2, access: { update: false, @@ -46,7 +48,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'cancelled', - name: i18n.t('Cancelled enrollments'), + name: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), order: 3, access: { update: false, @@ -59,5 +61,6 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, }, ], - [defaultAlteredTemplate, defaultTemplateId], + [defaultAlteredTemplate, defaultTemplateId, enrollmentsLabel], ); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx index be75d43236..ccb0b00f3f 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx @@ -16,6 +16,7 @@ import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import { useCompleteBulkEnrollments } from './hooks/useCompleteBulkEnrollments'; import { Widget } from '../../../../../Widget'; import type { PlainProps } from './CompleteAction.types'; +import { useTermLabel } from '../../../../../../metaData'; const styles: Readonly = { container: { @@ -36,9 +37,13 @@ const styles: Readonly = { }, }; -const getTooltipContent = (programDataWriteAccess: boolean, bulkDataEntryIsActive: boolean) => { +const getTooltipContent = ( + programDataWriteAccess: boolean, + bulkDataEntryIsActive: boolean, + enrollmentsLabel: string, +) => { if (!programDataWriteAccess) { - return i18n.t('You do not have access to bulk complete enrollments'); + return i18n.t('You do not have access to bulk complete {{enrollmentsLabel}}', { enrollmentsLabel }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -59,6 +64,8 @@ const CompleteActionPlain = ({ const [modalIsOpen, setModalIsOpen] = useState(false); const [completeEvents, setCompleteEvents] = useState(true); const [openAccordion, setOpenAccordion] = useState(false); + const enrollmentLabel = useTermLabel('enrollment', { programId }); + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); const { completeEnrollments, enrollmentCounts, @@ -75,7 +82,7 @@ const CompleteActionPlain = ({ onUpdateList, removeRowsFromSelection, }); - const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive); + const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive, enrollmentsLabel); const disabled = !programDataWriteAccess || bulkDataEntryIsActive; const ModalTextContent = () => { @@ -96,8 +103,12 @@ const CompleteActionPlain = ({ {hasPartiallyUploadedEnrollments ? // eslint-disable-next-line max-len - i18n.t('Some enrollments were completed successfully, but there was an error while completing the rest. Please see the details below.') : - i18n.t('There was an error while completing the enrollments. Please see the details below.') + i18n.t('Some {{enrollmentsLabel}} were completed successfully, but there was an error while completing the rest. Please see the details below.', { enrollmentsLabel }) : + i18n.t( + // eslint-disable-next-line max-len + 'There was an error while completing the {{enrollmentsLabel}}. Please see the details below.', + { enrollmentsLabel }, + ) } @@ -129,7 +140,10 @@ const CompleteActionPlain = ({ if (errorFetchingTrackedEntities) { return (
- {i18n.t('An unexpected error occurred while fetching the enrollments. Please try again.')} + {i18n.t( + 'An unexpected error occurred while fetching the {{enrollmentsLabel}}. Please try again.', + { enrollmentsLabel }, + )}
); } @@ -138,35 +152,41 @@ const CompleteActionPlain = ({ if (enrollmentCounts.active === 0) { return (
- {i18n.t('There are currently no active enrollments in the selection.')} + {i18n.t('There are currently no active {{enrollmentsLabel}} in the selection.', { enrollmentsLabel })} {' '} - {i18n.t('All enrollments are already completed or cancelled.')} + {i18n.t('All {{enrollmentsLabel}} are already completed or cancelled.', { enrollmentsLabel })}
); } return (
- {i18n.t('This action will complete {{count}} active enrollment in your selection.', + {i18n.t('This action will complete {{count}} active {{enrollmentLabel}} in your selection.', { count: enrollmentCounts.active, - defaultValue: 'This action will complete {{count}} active enrollment in your selection.', - defaultValue_plural: 'This action will complete {{count}} active enrollments in your selection.', + enrollmentLabel, + defaultValue: 'This action will complete {{count}} active {{enrollmentLabel}} in your selection.', + // eslint-disable-next-line max-len + defaultValue_plural: 'This action will complete {{count}} active {{enrollmentsLabel}} in your selection.', + enrollmentsLabel, }) } {' '} {enrollmentCounts.completed > 0 && - i18n.t('{{count}} enrollment already marked as completed will not be changed.', { + i18n.t('{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', { count: enrollmentCounts.completed, - defaultValue: '{{count}} enrollment already marked as completed will not be changed.', - defaultValue_plural: '{{count}} enrollments already marked as completed will not be changed.', + enrollmentLabel, + defaultValue: '{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', + // eslint-disable-next-line max-len + defaultValue_plural: '{{count}} {{enrollmentsLabel}} already marked as completed will not be changed.', + enrollmentsLabel, }) } setCompleteEvents(prevState => !prevState)} /> @@ -186,7 +206,7 @@ const CompleteActionPlain = ({ disabled={disabled} onClick={() => setModalIsOpen(true)} > - {i18n.t('Complete enrollments')} + {i18n.t('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -196,8 +216,8 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-enrollments-dialog'} > - {validationError ? i18n.t('Error completing enrollments') - : i18n.t('Complete enrollments')} + {validationError ? i18n.t('Error completing {{enrollmentsLabel}}', { enrollmentsLabel }) + : i18n.t('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -215,7 +235,7 @@ const CompleteActionPlain = ({ {!validationError && ( diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts index 30441f31c0..2606d43cb6 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts @@ -7,6 +7,7 @@ import { errorCreator, FEATURES, featureAvailable } from 'capture-core-utils'; import { ReactQueryAppNamespace, useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; import type { ProgramStage } from '../../../../../../../metaData'; +import { useTermLabel } from '../../../../../../../metaData'; type Props = { selectedRows: Record; @@ -93,6 +94,7 @@ export const useCompleteBulkEnrollments = ({ ({ message }) => message, { critical: true }, ); + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); const removeQueries = () => { queryClient.removeQueries( @@ -165,7 +167,7 @@ export const useCompleteBulkEnrollments = ({ }, onError: (serverResponse, variables) => { removeQueries(); - showAlert({ message: i18n.t('An error occurred when completing the enrollments') }); + showAlert({ message: i18n.t('An error occurred when completing the {{enrollmentsLabel}}', { enrollmentsLabel }) }); // eslint-disable-line max-len log.error( errorCreator('An error occurred when completing enrollments')({ serverResponse, @@ -190,7 +192,7 @@ export const useCompleteBulkEnrollments = ({ onUpdateList(true); }, onError: (serverResponse, variables) => { - showAlert({ message: i18n.t('An error occurred when completing the enrollments') }); + showAlert({ message: i18n.t('An error occurred when completing the {{enrollmentsLabel}}', { enrollmentsLabel }) }); // eslint-disable-line max-len log.error( errorCreator('An error occurred when completing enrollments')({ serverResponse, @@ -224,7 +226,8 @@ export const useCompleteBulkEnrollments = ({ serverResponse, enrollments, })); - showAlert({ message: i18n.t('An unknown error occurred when completing enrollments') }); + // eslint-disable-next-line max-len + showAlert({ message: i18n.t('An unknown error occurred when completing {{enrollmentsLabel}}', { enrollmentsLabel }) }); return; } const validEnrollments = filterValidEnrollments(enrollments, errors); diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx index f6d1f5051a..6713804c5f 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx @@ -5,10 +5,15 @@ import { useAuthority } from '../../../../../../utils/userInfo/useAuthority'; import { EnrollmentDeleteModal } from './EnrollmentDeleteModal'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import type { PlainProps } from './DeleteEnrollmentsAction.types'; +import { useTermLabel } from '../../../../../../metaData'; -const getTooltipContent = (programDataWriteAccess: boolean, bulkDataEntryIsActive: boolean) => { +const getTooltipContent = ( + programDataWriteAccess: boolean, + bulkDataEntryIsActive: boolean, + enrollmentsLabel: string, +) => { if (!programDataWriteAccess) { - return i18n.t('You do not have access to delete enrollments'); + return i18n.t('You do not have access to delete {{enrollmentsLabel}}', { enrollmentsLabel }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -27,7 +32,8 @@ export const DeleteEnrollmentsAction = ({ }: PlainProps) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const { hasAuthority } = useAuthority({ authority: CASCADE_DELETE_TEI_AUTHORITY }); - const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive); + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); + const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive, enrollmentsLabel); const disabled = !programDataWriteAccess || bulkDataEntryIsActive; if (!hasAuthority) { @@ -45,7 +51,7 @@ export const DeleteEnrollmentsAction = ({ disabled={disabled} onClick={() => setIsDeleteDialogOpen(true)} > - {i18n.t('Delete enrollments')} + {i18n.t('Delete {{enrollmentsLabel}}', { enrollmentsLabel })} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx index 67b3b227f1..fd6953c0ed 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx @@ -5,6 +5,7 @@ import i18n from '@dhis2/d2-i18n'; import { useDeleteEnrollments } from '../hooks/useDeleteEnrollments'; import { CustomCheckbox } from './CustomCheckbox'; import type { PlainProps } from './EnrollmentDeleteModal.types'; +import { useTermLabel } from '../../../../../../../metaData'; const styles: Readonly = { modalContent: { @@ -26,6 +27,8 @@ const EnrollmentDeleteModalPlain = ({ setIsDeleteDialogOpen, classes, }: PlainProps & WithStyles) => { + const enrollmentLabel = useTermLabel('enrollment', { programId }); + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); const { deleteEnrollments, isDeletingEnrollments, @@ -49,12 +52,15 @@ const EnrollmentDeleteModalPlain = ({ small > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })}
- {i18n.t('An error occurred while loading the selected enrollments. Please try again.')} + {i18n.t( + 'An error occurred while loading the selected {{enrollmentsLabel}}. Please try again.', + { enrollmentsLabel }, + )}
@@ -78,7 +84,7 @@ const EnrollmentDeleteModalPlain = ({ onClose={() => setIsDeleteDialogOpen(false)} > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -107,24 +113,24 @@ const EnrollmentDeleteModalPlain = ({ dataTest={'bulk-delete-enrollments-dialog'} > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })}
{/* eslint-disable-next-line max-len */} - {i18n.t('This action will permanently delete the selected enrollments, including all associated data and events.')} + {i18n.t('This action will permanently delete the selected {{enrollmentsLabel}}, including all associated data and events.', { enrollmentsLabel })}
- {i18n.t('Please select which enrollment statuses you want to delete:')} + {i18n.t('Please select which {{enrollmentLabel}} statuses you want to delete:', { enrollmentLabel })}
- {i18n.t('Delete {{count}} enrollment', { + {i18n.t('Delete {{count}} {{enrollmentLabel}}', { count: numberOfEnrollmentsToDelete, - defaultValue: 'Delete {{count}} enrollment', - defaultValue_plural: 'Delete {{count}} enrollments', + enrollmentLabel, + defaultValue: 'Delete {{count}} {{enrollmentLabel}}', + defaultValue_plural: 'Delete {{count}} {{enrollmentsLabel}}', + enrollmentsLabel, })} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts index 91321a3fab..17a1a0ba75 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts @@ -6,6 +6,7 @@ import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator, FEATURES, featureAvailable } from 'capture-core-utils'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; import { ReactQueryAppNamespace, useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; +import { useTermLabel } from '../../../../../../../metaData'; type Props = { selectedRows: Record; @@ -33,6 +34,7 @@ export const useDeleteEnrollments = ({ ({ message }) => message, { critical: true }, ); + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); const updateStatusToDelete = useCallback((status: string) => { setStatusToDelete(prevStatus => ({ @@ -86,7 +88,7 @@ export const useDeleteEnrollments = ({ { onError: (error) => { log.error(errorCreator('An error occurred when deleting enrollments')({ error })); - showAlert({ message: i18n.t('An error occurred when deleting enrollments') }); + showAlert({ message: i18n.t('An error occurred when deleting {{enrollmentsLabel}}', { enrollmentsLabel }) }); }, onSuccess: () => { queryClient.removeQueries([ReactQueryAppNamespace, ...QueryKey]); diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx index 44daa45d42..99f9c876cd 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx @@ -4,6 +4,7 @@ import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } fr import { useAuthority } from '../../../../../../utils/userInfo/useAuthority'; import { useCascadeDeleteTei } from './hooks/useCascadeDeleteTei'; import type { PlainProps } from './DeleteTeiAction.types'; +import { useTermLabel } from '../../../../../../metaData'; const CASCADE_DELETE_TEI_AUTHORITY = 'F_TEI_CASCADE_DELETE'; @@ -17,6 +18,7 @@ export const DeleteTeiAction = ({ }: PlainProps) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const { hasAuthority } = useAuthority({ authority: CASCADE_DELETE_TEI_AUTHORITY }); + const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); const { deleteTeis, isLoading } = useCascadeDeleteTei({ selectedRows, setIsDeleteDialogOpen, @@ -53,7 +55,10 @@ export const DeleteTeiAction = ({ - {i18n.t('Deleting records will also delete any associated enrollments and events.')} + {i18n.t( + 'Deleting records will also delete any associated {{enrollmentsLabel}} and events.', + { enrollmentsLabel }, + )} {' '} {i18n.t('This cannot be undone.')} {' '} From cd907e7b4b49cdeb35d5bfee017e5b380f8a782d Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:58:48 +0000 Subject: [PATCH 031/118] feat: implement tLabel for custom label handling for enrollment --- i18n/en.pot | 37 +------------------ .../Enrollment/MissingMessage.component.tsx | 6 +-- .../Enrollment/epics/fetchEnrollment.epics.ts | 13 ++++--- .../Actions/AddNew/AddNew.component.tsx | 8 +++- .../DeleteModal/DeleteModal.component.tsx | 5 ++- .../DeleteTeiAction/DeleteTeiAction.tsx | 5 ++- .../metaData/helpers/customLabels/index.ts | 2 + .../metaData/helpers/customLabels/tLabel.ts | 30 +++++++++++++++ .../capture-core/metaData/helpers/index.ts | 10 ++++- .../capture-core/metaData/index.ts | 2 + .../feedback.reducerDescriptionGetter.ts | 25 +++++++++---- .../utils/capitalizeFirstLetter.ts | 11 ++++++ 12 files changed, 98 insertions(+), 56 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts create mode 100644 src/core_modules/capture-core/utils/capitalizeFirstLetter.ts diff --git a/i18n/en.pot b/i18n/en.pot index 3192e743c8..35b5761d7c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-15T18:43:50.743Z\n" -"PO-Revision-Date: 2026-08-15T18:43:50.743Z\n" +"POT-Creation-Date: 2026-08-15T18:58:49.351Z\n" +"PO-Revision-Date: 2026-08-15T18:58:49.351Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -775,9 +775,6 @@ msgstr "" msgid "Enroll a new {{selectedTetName}} in this program." msgstr "Enroll a new {{selectedTetName}} in this program." -msgid "{{programName}} is an event program and does not have enrollments." -msgstr "{{programName}} is an event program and does not have enrollments." - msgid "Create a new event in this program." msgstr "Create a new event in this program." @@ -793,9 +790,6 @@ msgstr "Tracked entity instance with id \"{{teiId}}\" does not exist" msgid "Program with id \"{{programId}}\" does not exist" msgstr "Program with id \"{{programId}}\" does not exist" -msgid "An error occurred while fetching enrollments. Please enter a valid url." -msgstr "An error occurred while fetching enrollments. Please enter a valid url." - msgid "An error has occurred" msgstr "An error has occurred" @@ -1344,9 +1338,6 @@ msgstr "{{enrollmentLabel}} actions" msgid "We are processing your request." msgstr "We are processing your request." -msgid "Only one enrollment per {{tetName}} is allowed in this program" -msgstr "Only one enrollment per {{tetName}} is allowed in this program" - msgid "Add new" msgstr "Add new" @@ -1674,15 +1665,6 @@ msgstr "You do not have access to delete this {{trackedEntityTypeName}}" msgid "Delete {{trackedEntityTypeName}}" msgstr "Delete {{trackedEntityTypeName}}" -msgid "" -"Are you sure you want to delete this {{trackedEntityTypeName}}? This will " -"permanently remove the {{trackedEntityTypeName}} and all its associated " -"enrollments and events in all programs." -msgstr "" -"Are you sure you want to delete this {{trackedEntityTypeName}}? This will " -"permanently remove the {{trackedEntityTypeName}} and all its associated " -"enrollments and events in all programs." - msgid "There was a problem deleting the {{trackedEntityTypeName}}" msgstr "There was a problem deleting the {{trackedEntityTypeName}}" @@ -2144,9 +2126,6 @@ msgstr[1] "Delete {{count}} {{enrollmentsLabel}}" msgid "An error occurred when deleting {{enrollmentsLabel}}" msgstr "An error occurred when deleting {{enrollmentsLabel}}" -msgid "Delete {{ trackedEntityName }} with all enrollments" -msgstr "Delete {{ trackedEntityName }} with all enrollments" - msgid "Delete {{count}} {{ trackedEntityName }}" msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" @@ -2311,24 +2290,12 @@ msgstr "Organisation unit search failed." msgid "Error saving tracked entity instance" msgstr "Error saving tracked entity instance" -msgid "Error saving enrollment" -msgstr "Error saving enrollment" - -msgid "Error saving the enrollment event" -msgstr "Error saving the enrollment event" - -msgid "Error deleting the enrollment event" -msgstr "Error deleting the enrollment event" - msgid "Error editing the event, the changes made were not saved" msgstr "Error editing the event, the changes made were not saved" msgid "Error updating the Assignee" msgstr "Error updating the Assignee" -msgid "Could not save enrollment note" -msgstr "Could not save enrollment note" - msgid "Could not save event note" msgstr "Could not save event note" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index 800696f386..49e1f3c7de 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -5,7 +5,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { useMissingCategoriesInProgramSelection } from '../../../hooks/useMissingCategoriesInProgramSelection'; import { scopeTypes } from '../../../metaData/helpers/constants'; -import { useTermLabel } from '../../../metaData'; +import { useTermLabel, tLabel } from '../../../metaData'; import { enrollmentAccessLevels } from './EnrollmentPage.constants'; import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; @@ -302,8 +302,8 @@ const MissingMessagePlain = ({ missingStatus === missingStatuses.EVENT_PROGRAM_SELECTED &&
- {i18n.t('{{programName}} is an event program and does not have enrollments.', { - programName, interpolation: { escapeValue: false }, + {tLabel('{{programName}} is an event program and does not have {{enrollmentsLabel}}.', { + programName, enrollmentsLabel, interpolation: { escapeValue: false }, })}
{ +const handleErrorsFromOlderBackends = (error: any, programId?: string) => { const { message } = error || {}; if (message) { if (message.includes(serverErrorMessages.OWNERSHIP_ACCESS_PARTIALLY_DENIED)) { @@ -137,7 +138,9 @@ const handleErrorsFromOlderBackends = (error: any) => { return fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.NO_ACCESS }); } } - const errorMessage = i18n.t('An error occurred while fetching enrollments. Please enter a valid url.'); + const enrollmentsLabel = getTermLabel(programId, 'enrollment', { plural: true }); + const errorMessage = + tLabel('An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', { enrollmentsLabel }); return showErrorViewOnEnrollmentPage({ error: errorMessage }); }; @@ -165,7 +168,7 @@ export const fetchEnrollmentsEpic = (action$: any, store: any, { querySingleReso querySingleResource, }); } - return of(handleErrorsFromOlderBackends(error)); + return of(handleErrorsFromOlderBackends(error, programId)); }), map((action: any) => verifyFetchedEnrollments({ teiId, programId, action })), ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx index d5e0fc0ef6..9b6e4ab68e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx @@ -2,16 +2,22 @@ import React from 'react'; import { IconAdd16, MenuItem } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; +import { useTermLabel, tLabel } from '../../../../metaData'; import type { Props } from './addNew.types'; export const AddNew = ({ tetName, canAddNew, onlyEnrollOnce, onAddNew }: Props) => { + const enrollmentLabel = useTermLabel('enrollment'); + if (!canAddNew) { return null; } return ( { const [errorReports, setErrorReports] = useState>([]); + const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); const handleErrors = (errors: Array) => { setErrorReports(errors); }; @@ -23,9 +25,10 @@ export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, setOpenModal

{/* eslint-disable-next-line max-len */} - {i18n.t('Are you sure you want to delete this {{trackedEntityTypeName}}? This will permanently remove the {{trackedEntityTypeName}} and all its associated enrollments and events in all programs.', + {tLabel('Are you sure you want to delete this {{trackedEntityTypeName}}? This will permanently remove the {{trackedEntityTypeName}} and all its associated {{enrollmentsLabel}} and events in all programs.', { trackedEntityTypeName, + enrollmentsLabel, interpolation: { escapeValue: false }, }, )} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx index 99f9c876cd..8205f6958d 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx @@ -4,7 +4,7 @@ import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } fr import { useAuthority } from '../../../../../../utils/userInfo/useAuthority'; import { useCascadeDeleteTei } from './hooks/useCascadeDeleteTei'; import type { PlainProps } from './DeleteTeiAction.types'; -import { useTermLabel } from '../../../../../../metaData'; +import { useTermLabel, tLabel } from '../../../../../../metaData'; const CASCADE_DELETE_TEI_AUTHORITY = 'F_TEI_CASCADE_DELETE'; @@ -35,8 +35,9 @@ export const DeleteTeiAction = ({ small onClick={() => setIsDeleteDialogOpen(true)} > - {i18n.t('Delete {{ trackedEntityName }} with all enrollments', { + {tLabel('Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}', { trackedEntityName: trackedEntityName.toLowerCase(), + enrollmentsLabel, })} diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts index 08d6759b09..9c9de5821a 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -1,3 +1,5 @@ export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; export { getTermLabel, useTermLabel } from './useLabel'; +export { tLabel } from './tLabel'; +export { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts new file mode 100644 index 0000000000..172b02b9c5 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts @@ -0,0 +1,30 @@ +import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; + +const getRawTranslation = (key: string): string => + (i18n as any).getResource((i18n as any).language, 'default', key) + ?? (i18n as any).getResource('en', 'default', key) + ?? key; + +const startsWithVar = (raw: string, varName: string): boolean => { + const trimmed = raw.trimStart(); + return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); +}; + +/** + * Drop-in replacement for i18n.t() when the string contains custom label variables. + * Automatically capitalizes a variable's value when it appears as the first word + * in the translated string — without requiring any changes to translation files. + */ +export const tLabel = (key: string, options: Record = {}): string => { + const raw = getRawTranslation(key); + const processedOptions = { ...options }; + + for (const [varName, value] of Object.entries(options)) { + if (typeof value === 'string' && startsWithVar(raw, varName)) { + processedOptions[varName] = capitalizeFirstLetter(value); + } + } + + return i18n.t(key, { ...processedOptions, interpolation: { escapeValue: false } }); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index c46d3adf0e..94a88e45ee 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -17,5 +17,13 @@ export { getScopeInfo } from './getScopeInfo'; export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; -export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, useTermLabel } from './customLabels'; +export { + CUSTOM_LABEL_FIELDS, + extractCustomLabels, + resolveLabel, + getTermLabel, + useTermLabel, + tLabel, + capitalizeFirstLetter, +} from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 139fb995a4..a098e321b4 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -45,5 +45,7 @@ export { resolveLabel, getTermLabel, useTermLabel, + tLabel, + capitalizeFirstLetter, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts index b03059cd59..dc816ccd1b 100644 --- a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts +++ b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts @@ -5,6 +5,7 @@ import isString from 'd2-utilizr/lib/isString'; import isObject from 'd2-utilizr/lib/isObject'; import uuid from 'd2-utilizr/lib/uuid'; import { errorCreator } from 'capture-core-utils'; +import { getTermLabel, tLabel } from '../../metaData'; import { createReducerDescription } from '../../trackerRedux/trackerReducer'; import { actionTypes as feedbackActionTypes } from '../../components/FeedbackBar/actions/feedback.actions'; import { actionTypes as dataEntryActionTypes } from '../../components/DataEntry/actions/dataEntry.actions'; @@ -109,12 +110,18 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback({ message: i18n.t('Organisation unit search failed.') }), [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error saving tracked entity instance') }), - [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error saving enrollment') }), - [enrollmentSiteActionTypes.SAVE_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error saving the enrollment event') }), - [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error deleting the enrollment event') }), + [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: () => { + const enrollmentLabel = getTermLabel(undefined, 'enrollment'); + return addErrorFeedback({ message: tLabel('Error saving {{enrollmentLabel}}', { enrollmentLabel }) }); + }, + [enrollmentSiteActionTypes.SAVE_FAILED]: () => { + const enrollmentLabel = getTermLabel(undefined, 'enrollment'); + return addErrorFeedback({ message: tLabel('Error saving the {{enrollmentLabel}} event', { enrollmentLabel }) }); + }, + [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: () => { + const enrollmentLabel = getTermLabel(undefined, 'enrollment'); + return addErrorFeedback({ message: tLabel('Error deleting the {{enrollmentLabel}} event', { enrollmentLabel }) }); + }, [editEventDataEntryAction.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED]: () => addErrorFeedback({ message: i18n.t('Error editing the event, the changes made were not saved') }), [enrollmentSiteActionTypes.ERROR_ENROLLMENT]: (_state, action) => @@ -123,8 +130,10 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), [enrollmentEditEventActionTypes.ASSIGNEE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), - [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: () => - addErrorFeedback({ message: i18n.t('Could not save enrollment note') }), + [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: () => { + const enrollmentLabel = getTermLabel(undefined, 'enrollment'); + return addErrorFeedback({ message: tLabel('Could not save {{enrollmentLabel}} note', { enrollmentLabel }) }); + }, [eventNoteActionTypes.ADD_NOTE_FAILED_FOR_EVENT]: () => addErrorFeedback({ message: i18n.t('Could not save event note') }), [viewEventNotesActionTypes.SAVE_EVENT_NOTE_FAILED]: () => diff --git a/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts b/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts new file mode 100644 index 0000000000..cf3370402a --- /dev/null +++ b/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts @@ -0,0 +1,11 @@ +import i18n from '@dhis2/d2-i18n'; + +export const capitalizeFirstLetter = (str: string): string => { + if (!str) return str; + const locale = (i18n as any).language ?? 'en'; + try { + return str.charAt(0).toLocaleUpperCase(locale) + str.slice(1); + } catch { + return str.charAt(0).toUpperCase() + str.slice(1); + } +}; From 89b75c7e14366034438e3a6b8f90feda39af9214 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:42:26 +0000 Subject: [PATCH 032/118] fix: temp clean up --- cypress/e2e/ScopeSelector/ScopeSelector.js | 2 +- .../WidgetEnrollmentNote/index.js | 2 +- i18n/en.pot | 25 ++++- .../Relationships/Relationships.component.tsx | 9 +- .../WidgetEnrollment.component.tsx | 11 +-- .../WidgetProfile/hooks/useApiProgram.ts | 13 +-- .../metaData/helpers/customLabels.ts | 97 +++++++++++++++++++ .../helpers/customLabels/customLabels.ts | 51 ---------- .../metaData/helpers/customLabels/index.ts | 5 - .../metaData/helpers/customLabels/useLabel.ts | 58 ----------- .../capture-core/metaData/helpers/index.ts | 2 - .../capture-core/metaData/index.ts | 2 - .../helpers/customLabels => utils}/tLabel.ts | 7 +- 13 files changed, 136 insertions(+), 148 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/index.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts rename src/core_modules/capture-core/{metaData/helpers/customLabels => utils}/tLabel.ts (72%) diff --git a/cypress/e2e/ScopeSelector/ScopeSelector.js b/cypress/e2e/ScopeSelector/ScopeSelector.js index b3298a75e6..90e7a8e300 100644 --- a/cypress/e2e/ScopeSelector/ScopeSelector.js +++ b/cypress/e2e/ScopeSelector/ScopeSelector.js @@ -284,7 +284,7 @@ And('you see the enrollment event Edit page but there is no org unit id in the u And('you see the enrollment event New page but there is no stage id in the url', () => { cy.url().should('eq', `${Cypress.config().baseUrl}/#/enrollmentEventNew?enrollmentId=Aemr3Q02aqV&orgUnitId=DiszpKrYNg8&programId=ur1Edk5Oe2n&teiId=eUTmQGull6H`); - cy.contains('Choose a stage for a new event'); + cy.contains('Choose a program stage for a new event'); }); And('you see the enrollment page without org unit in the url', () => { diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js index 67f9b5df5f..e96b83be28 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js @@ -3,7 +3,7 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; const timeStamp = Math.round((new Date()).getTime() / 1000); Then('the stages and events should be loaded', () => { - cy.contains('Stages and Events').should('exist'); + cy.contains('Program stages and events').should('exist'); }); When(/^you fill in the note: (.*)$/, (note) => { diff --git a/i18n/en.pot b/i18n/en.pot index c79ff5717a..95b0ec811f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-17T07:54:36.289Z\n" -"PO-Revision-Date: 2026-08-17T07:54:36.289Z\n" +"POT-Creation-Date: 2026-08-28T13:42:27.818Z\n" +"PO-Revision-Date: 2026-08-28T13:42:27.818Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2239,24 +2239,45 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" +msgid "enrollments" +msgstr "enrollments" + msgid "program stage" msgstr "program stage" +msgid "program stages" +msgstr "program stages" + msgid "note" msgstr "note" +msgid "notes" +msgstr "notes" + msgid "relationship" msgstr "relationship" +msgid "relationships" +msgstr "relationships" + msgid "attribute" msgstr "attribute" +msgid "attributes" +msgstr "attributes" + msgid "organisation unit" msgstr "organisation unit" +msgid "organisation units" +msgstr "organisation units" + msgid "follow-up" msgstr "follow-up" +msgid "follow-ups" +msgstr "follow-ups" + msgid "Program not found" msgstr "Program not found" diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index 7b8ccb42e9..69c2596a9f 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -63,10 +63,9 @@ const styles: Readonly = (theme: any) => ({ }, }); -const getFromName = (entityType: string) => { - if (entityType === 'PROGRAM_STAGE_INSTANCE') return i18n.t('This event'); - return undefined; -}; +const getFromNames = () => ({ + PROGRAM_STAGE_INSTANCE: i18n.t('This event'), +}); type PlainProps = { relationships: Array; @@ -106,7 +105,7 @@ class RelationshipsPlain extends React.Component { const { onRenderConnectedEntity } = this.props; if (entity.id === this.props.currentEntityId) { - return getFromName(entity.type); + return getFromNames()[entity.type]; } return onRenderConnectedEntity ? onRenderConnectedEntity(entity) : entity.name; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 3bff464153..48fb186023 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -17,12 +17,9 @@ import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; -import { - dataElementTypes, - useTermLabel, - tLabel, - capitalizeFirstLetter, -} from '../../metaData'; +import { dataElementTypes, useTermLabel } from '../../metaData'; +import { tLabel } from '../../utils/tLabel'; +import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'; import { convertValue } from '../../converters/clientToView'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; @@ -121,7 +118,7 @@ const WidgetEnrollmentPlain = ({ onClose={useCallback(() => setOpenStatus(false), [setOpenStatus])} open={open} > - {true && ( + {initError && (

{tLabel('{{enrollmentLabel}} widget could not be loaded. Please try again later', { enrollmentLabel })} diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts index 97fae559f4..ada6c9dde0 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts @@ -1,12 +1,7 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; -const trackedEntityTypeFields = - 'id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + - 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + - 'translations[property,locale,value]'; - -const buildFields = (): string => +const fields = 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + 'displayIncidentDate,access[*],' + @@ -28,7 +23,9 @@ const buildFields = (): string => 'displayDescription,valueType,optionSetValue,unique,orgunitScope,pattern,translations[property,locale,value],' + 'optionSet[id,displayName,version,valueType,options[id,displayName,name,code,style,translations]]],' + 'displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate],' + - `trackedEntityType[${trackedEntityTypeFields}],` + + 'trackedEntityType[id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + + 'translations[property,locale,value]],' + 'userRoles[id,displayName]'; export const useApiProgram = (programId: string) => { @@ -39,7 +36,7 @@ export const useApiProgram = (programId: string) => { resource: 'programs', id: programId, params: { - fields: buildFields(), + fields, }, }, }), diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts new file mode 100644 index 0000000000..d50506e143 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -0,0 +1,97 @@ +import i18n from '@dhis2/d2-i18n'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { programCollection } from '../../metaDataMemoryStores'; + +type CustomLabelField = { + field?: string, + pluralField?: string, +}; + +export const CUSTOM_LABEL_FIELDS = { + enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, + event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, + programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, + note: { field: 'displayNoteLabel' }, + relationship: { field: 'displayRelationshipLabel' }, + attribute: { field: 'displayTrackedEntityAttributeLabel' }, + orgUnit: { field: 'displayOrgUnitLabel' }, + followUp: { field: 'displayFollowUpLabel' }, +} as const satisfies { [key: string]: CustomLabelField }; + +export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; +export type CustomLabels = Record; +export type LabelOptions = { plural?: boolean }; + +const allFields: Array = Array.from( + new Set( + Object.values(CUSTOM_LABEL_FIELDS) + .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) + .filter((field): field is string => Boolean(field)), + ), +); + +export const extractCustomLabels = (cached: Record): CustomLabels => { + const labels: CustomLabels = {}; + allFields.forEach((field) => { + if (cached[field]) labels[field] = cached[field]; + }); + return labels; +}; + +type LabelSource = CustomLabels | undefined | null; + +export const resolveLabel = ( + sources: LabelSource | Array, + key: CustomLabelKey, + { plural = false }: LabelOptions = {}, +): string | undefined => { + const term = CUSTOM_LABEL_FIELDS[key] as CustomLabelField; + const list = Array.isArray(sources) ? sources : [sources]; + const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); + return pick(plural ? term.pluralField : term.field); +}; + +const defaults: Record string; plural: () => string }> = { + enrollment: { singular: () => i18n.t('enrollment'), plural: () => i18n.t('enrollments') }, + event: { singular: () => i18n.t('event'), plural: () => i18n.t('events') }, + programStage: { singular: () => i18n.t('program stage'), plural: () => i18n.t('program stages') }, + note: { singular: () => i18n.t('note'), plural: () => i18n.t('notes') }, + relationship: { singular: () => i18n.t('relationship'), plural: () => i18n.t('relationships') }, + attribute: { singular: () => i18n.t('attribute'), plural: () => i18n.t('attributes') }, + orgUnit: { singular: () => i18n.t('organisation unit'), plural: () => i18n.t('organisation units') }, + followUp: { singular: () => i18n.t('follow-up'), plural: () => i18n.t('follow-ups') }, +}; + +type TermLabelOptions = LabelOptions & { stageId?: string; programId?: string }; + +const resolveTerm = ( + programId: string | undefined, + key: CustomLabelKey, + { stageId, plural = false }: TermLabelOptions, +): string => { + const program = programId ? programCollection.get(programId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + const customLabel = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); + if (customLabel) return customLabel; + return plural ? defaults[key].plural() : defaults[key].singular(); +}; + +export const getTermLabel = ( + programId: string | undefined, + key: CustomLabelKey, + options: TermLabelOptions = {}, +): string => resolveTerm(programId, key, options); + +export const useTermLabel = ( + key: CustomLabelKey, + options: TermLabelOptions = {}, +): string => { + const { programId, stageId, plural } = options; + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const id = programId ?? currentProgramId; + return useMemo( + () => resolveTerm(id, key, { stageId, plural }), + [id, key, stageId, plural], + ); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts deleted file mode 100644 index bf60ed5c72..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ /dev/null @@ -1,51 +0,0 @@ -type CustomLabelField = { - field?: string, - pluralField?: string, -}; - -export const CUSTOM_LABEL_FIELDS = { - enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - note: { field: 'displayNoteLabel' }, - relationship: { field: 'displayRelationshipLabel' }, - attribute: { field: 'displayTrackedEntityAttributeLabel' }, - orgUnit: { field: 'displayOrgUnitLabel' }, - followUp: { field: 'displayFollowUpLabel' }, -} as const satisfies { [key: string]: CustomLabelField }; - -export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; -export type CustomLabels = Record; -export type LabelOptions = { plural?: boolean }; - -const allFields: Array = Array.from( - new Set( - Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) - .filter((field): field is string => Boolean(field)), - ), -); - -export const extractCustomLabels = (cached: Record): CustomLabels => { - const labels: CustomLabels = {}; - allFields.forEach((field) => { - if (cached[field]) labels[field] = cached[field]; - }); - return labels; -}; - -type LabelSource = CustomLabels | undefined | null; - -export const resolveLabel = ( - sources: LabelSource | Array, - key: CustomLabelKey, - { plural = false }: LabelOptions = {}, -): string | undefined => { - const term = CUSTOM_LABEL_FIELDS[key] as CustomLabelField; - const list = Array.isArray(sources) ? sources : [sources]; - const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); - if (plural && term.pluralField) { - return pick(term.pluralField); - } - return pick(term.field); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts deleted file mode 100644 index 9c9de5821a..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel } from './customLabels'; -export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; -export { getTermLabel, useTermLabel } from './useLabel'; -export { tLabel } from './tLabel'; -export { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts deleted file mode 100644 index dae529d57f..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ /dev/null @@ -1,58 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { programCollection } from '../../../metaDataMemoryStores'; -import { resolveLabel } from './customLabels'; -import type { CustomLabelKey, LabelOptions } from './customLabels'; - -const defaults: Record string> = { - enrollment: () => i18n.t('enrollment'), - event: () => i18n.t('event'), - programStage: () => i18n.t('program stage'), - note: () => i18n.t('note'), - relationship: () => i18n.t('relationship'), - attribute: () => i18n.t('attribute'), - orgUnit: () => i18n.t('organisation unit'), - followUp: () => i18n.t('follow-up'), -}; - -type TermLabelOptions = LabelOptions & { stageId?: string }; - -const resolve = ( - programId: string | undefined, - key: CustomLabelKey, - { stageId, plural }: TermLabelOptions, -): string => { - const program = programId ? programCollection.get(programId) : undefined; - const stage = program && stageId ? program.getStage(stageId) : undefined; - const customLabel = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); - return customLabel ?? defaults[key](); -}; - -/** - * Works anywhere — components, reducers, epics. - * Returns the custom label from the program (or stage), falling back to the - * translated default term. - */ -export const getTermLabel = ( - programId: string | undefined, - key: CustomLabelKey, - options: TermLabelOptions = {}, -): string => resolve(programId, key, options); - -/** - * React hook version — reads programId from Redux automatically. - * Pass programId explicitly to override (e.g. cross-program widgets). - */ -export const useTermLabel = ( - key: CustomLabelKey, - options: TermLabelOptions & { programId?: string } = {}, -): string => { - const { programId, stageId, plural } = options; - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const id = programId ?? currentProgramId; - return useMemo( - () => resolve(id, key, { stageId, plural }), - [id, key, stageId, plural], - ); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index 94a88e45ee..ac10105196 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -23,7 +23,5 @@ export { resolveLabel, getTermLabel, useTermLabel, - tLabel, - capitalizeFirstLetter, } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index a098e321b4..139fb995a4 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -45,7 +45,5 @@ export { resolveLabel, getTermLabel, useTermLabel, - tLabel, - capitalizeFirstLetter, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts b/src/core_modules/capture-core/utils/tLabel.ts similarity index 72% rename from src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts rename to src/core_modules/capture-core/utils/tLabel.ts index 172b02b9c5..077c37d69c 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts +++ b/src/core_modules/capture-core/utils/tLabel.ts @@ -1,5 +1,5 @@ import i18n from '@dhis2/d2-i18n'; -import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; +import { capitalizeFirstLetter } from './capitalizeFirstLetter'; const getRawTranslation = (key: string): string => (i18n as any).getResource((i18n as any).language, 'default', key) @@ -11,11 +11,6 @@ const startsWithVar = (raw: string, varName: string): boolean => { return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); }; -/** - * Drop-in replacement for i18n.t() when the string contains custom label variables. - * Automatically capitalizes a variable's value when it appears as the first word - * in the translated string — without requiring any changes to translation files. - */ export const tLabel = (key: string, options: Record = {}): string => { const raw = getRawTranslation(key); const processedOptions = { ...options }; From a2dd40f0ed646860bd57c7618036f967b267e697 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:37:09 +0000 Subject: [PATCH 033/118] feat: replace tLabel with tCustomTerm --- i18n/en.pot | 19 +-- .../WidgetEnrollment.component.tsx | 8 +- .../metaData/helpers/customLabels.ts | 108 +++++++++++------- .../capture-core/metaData/helpers/index.ts | 1 - .../capture-core/metaData/index.ts | 1 - .../capture-core/utils/tCustomTerm.ts | 37 ++++++ src/core_modules/capture-core/utils/tLabel.ts | 25 ---- 7 files changed, 108 insertions(+), 91 deletions(-) create mode 100644 src/core_modules/capture-core/utils/tCustomTerm.ts delete mode 100644 src/core_modules/capture-core/utils/tLabel.ts diff --git a/i18n/en.pot b/i18n/en.pot index 95b0ec811f..bcfc414f2d 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-28T13:42:27.818Z\n" -"PO-Revision-Date: 2026-08-28T13:42:27.818Z\n" +"POT-Creation-Date: 2026-08-28T14:37:10.874Z\n" +"PO-Revision-Date: 2026-08-28T14:37:10.874Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2251,33 +2251,18 @@ msgstr "program stages" msgid "note" msgstr "note" -msgid "notes" -msgstr "notes" - msgid "relationship" msgstr "relationship" -msgid "relationships" -msgstr "relationships" - msgid "attribute" msgstr "attribute" -msgid "attributes" -msgstr "attributes" - msgid "organisation unit" msgstr "organisation unit" -msgid "organisation units" -msgstr "organisation units" - msgid "follow-up" msgstr "follow-up" -msgid "follow-ups" -msgstr "follow-ups" - msgid "Program not found" msgstr "Program not found" diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 48fb186023..2949fd4e17 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -18,7 +18,7 @@ import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDo import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes, useTermLabel } from '../../metaData'; -import { tLabel } from '../../utils/tLabel'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'; import { convertValue } from '../../converters/clientToView'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; @@ -120,8 +120,10 @@ const WidgetEnrollmentPlain = ({ > {initError && (
- {tLabel('{{enrollmentLabel}} widget could not be loaded. Please try again later', - { enrollmentLabel })} + {tCustomTerm( + '{{enrollmentLabel}} widget could not be loaded. Please try again later', + { enrollmentLabel }, + )}
)} {loading && } diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts index d50506e143..aa7964603b 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -3,41 +3,70 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { programCollection } from '../../metaDataMemoryStores'; -type CustomLabelField = { - field?: string, - pluralField?: string, +type LabelConfig = { + field: string; + pluralField?: string; + singular: () => string; + plural?: () => string; }; -export const CUSTOM_LABEL_FIELDS = { - enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - note: { field: 'displayNoteLabel' }, - relationship: { field: 'displayRelationshipLabel' }, - attribute: { field: 'displayTrackedEntityAttributeLabel' }, - orgUnit: { field: 'displayOrgUnitLabel' }, - followUp: { field: 'displayFollowUpLabel' }, -} as const satisfies { [key: string]: CustomLabelField }; +const asLabels = (labels: Record) => labels; -export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; +const LABELS = asLabels({ + enrollment: { + field: 'displayEnrollmentLabel', + pluralField: 'displayEnrollmentsLabel', + singular: () => i18n.t('enrollment'), + plural: () => i18n.t('enrollments'), + }, + event: { + field: 'displayEventLabel', + pluralField: 'displayEventsLabel', + singular: () => i18n.t('event'), + plural: () => i18n.t('events'), + }, + programStage: { + field: 'displayProgramStageLabel', + pluralField: 'displayProgramStagesLabel', + singular: () => i18n.t('program stage'), + plural: () => i18n.t('program stages'), + }, + note: { + field: 'displayNoteLabel', + singular: () => i18n.t('note'), + }, + relationship: { + field: 'displayRelationshipLabel', + singular: () => i18n.t('relationship'), + }, + attribute: { + field: 'displayTrackedEntityAttributeLabel', + singular: () => i18n.t('attribute'), + }, + orgUnit: { + field: 'displayOrgUnitLabel', + singular: () => i18n.t('organisation unit'), + }, + followUp: { + field: 'displayFollowUpLabel', + singular: () => i18n.t('follow-up'), + }, +}); + +export type CustomLabelKey = keyof typeof LABELS; export type CustomLabels = Record; export type LabelOptions = { plural?: boolean }; -const allFields: Array = Array.from( - new Set( - Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) - .filter((field): field is string => Boolean(field)), - ), +const ALL_FIELD_NAMES = Object.values(LABELS).flatMap( + ({ field, pluralField }) => (pluralField ? [field, pluralField] : [field]), ); -export const extractCustomLabels = (cached: Record): CustomLabels => { - const labels: CustomLabels = {}; - allFields.forEach((field) => { - if (cached[field]) labels[field] = cached[field]; - }); - return labels; -}; +export const extractCustomLabels = (cached: Record): CustomLabels => + Object.fromEntries( + ALL_FIELD_NAMES + .filter(field => typeof cached[field] === 'string') + .map(field => [field, cached[field] as string]), + ); type LabelSource = CustomLabels | undefined | null; @@ -46,21 +75,11 @@ export const resolveLabel = ( key: CustomLabelKey, { plural = false }: LabelOptions = {}, ): string | undefined => { - const term = CUSTOM_LABEL_FIELDS[key] as CustomLabelField; + const { field, pluralField } = LABELS[key]; + const target = plural ? pluralField : field; + if (!target) return undefined; const list = Array.isArray(sources) ? sources : [sources]; - const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); - return pick(plural ? term.pluralField : term.field); -}; - -const defaults: Record string; plural: () => string }> = { - enrollment: { singular: () => i18n.t('enrollment'), plural: () => i18n.t('enrollments') }, - event: { singular: () => i18n.t('event'), plural: () => i18n.t('events') }, - programStage: { singular: () => i18n.t('program stage'), plural: () => i18n.t('program stages') }, - note: { singular: () => i18n.t('note'), plural: () => i18n.t('notes') }, - relationship: { singular: () => i18n.t('relationship'), plural: () => i18n.t('relationships') }, - attribute: { singular: () => i18n.t('attribute'), plural: () => i18n.t('attributes') }, - orgUnit: { singular: () => i18n.t('organisation unit'), plural: () => i18n.t('organisation units') }, - followUp: { singular: () => i18n.t('follow-up'), plural: () => i18n.t('follow-ups') }, + return list.find(source => source?.[target])?.[target]; }; type TermLabelOptions = LabelOptions & { stageId?: string; programId?: string }; @@ -72,9 +91,10 @@ const resolveTerm = ( ): string => { const program = programId ? programCollection.get(programId) : undefined; const stage = program && stageId ? program.getStage(stageId) : undefined; - const customLabel = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); - if (customLabel) return customLabel; - return plural ? defaults[key].plural() : defaults[key].singular(); + const custom = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); + if (custom) return custom; + if (plural) return LABELS[key].plural?.() ?? `${key}s`; + return LABELS[key].singular(); }; export const getTermLabel = ( diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index ac10105196..b4158ea1a9 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -18,7 +18,6 @@ export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { - CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 139fb995a4..1e8e7461b6 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -40,7 +40,6 @@ export { getProgramThrowIfNotFound, getProgramAndStageForEventProgram, getEventProgramEventAccess, - CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, diff --git a/src/core_modules/capture-core/utils/tCustomTerm.ts b/src/core_modules/capture-core/utils/tCustomTerm.ts new file mode 100644 index 0000000000..dcef43f076 --- /dev/null +++ b/src/core_modules/capture-core/utils/tCustomTerm.ts @@ -0,0 +1,37 @@ +import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from './capitalizeFirstLetter'; + +type I18nInternal = { + getResource: (lang: string, ns: string, key: string) => string | undefined; + language: string; +}; +const internal = i18n as unknown as I18nInternal; + +const getRawTranslation = (key: string): string => + internal.getResource(internal.language, 'default', key) + ?? internal.getResource('en', 'default', key) + ?? key; + +const startsWithVar = (raw: string, varName: string): boolean => { + const trimmed = raw.trimStart(); + return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); +}; + +export const tCustomTerm = (key: string, options: Record = {}): string => { + const raw = getRawTranslation(key); + const { interpolation, ...values } = options; + + const cased = Object.fromEntries( + Object.entries(values).map(([name, value]) => [ + name, + typeof value === 'string' && startsWithVar(raw, name) + ? capitalizeFirstLetter(value) + : value, + ]), + ); + + return i18n.t(key, { + ...cased, + interpolation: { escapeValue: false, ...(interpolation as Record ?? {}) }, + }); +}; diff --git a/src/core_modules/capture-core/utils/tLabel.ts b/src/core_modules/capture-core/utils/tLabel.ts deleted file mode 100644 index 077c37d69c..0000000000 --- a/src/core_modules/capture-core/utils/tLabel.ts +++ /dev/null @@ -1,25 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { capitalizeFirstLetter } from './capitalizeFirstLetter'; - -const getRawTranslation = (key: string): string => - (i18n as any).getResource((i18n as any).language, 'default', key) - ?? (i18n as any).getResource('en', 'default', key) - ?? key; - -const startsWithVar = (raw: string, varName: string): boolean => { - const trimmed = raw.trimStart(); - return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); -}; - -export const tLabel = (key: string, options: Record = {}): string => { - const raw = getRawTranslation(key); - const processedOptions = { ...options }; - - for (const [varName, value] of Object.entries(options)) { - if (typeof value === 'string' && startsWithVar(raw, varName)) { - processedOptions[varName] = capitalizeFirstLetter(value); - } - } - - return i18n.t(key, { ...processedOptions, interpolation: { escapeValue: false } }); -}; From ff0098f81faded03db96ed77597b5c5f70da0e4e Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:48:18 +0000 Subject: [PATCH 034/118] Merge branch 'hv/chore/DHIS2-21969_refine-configurable-terminology-support' into hv/feat/DHIS2-21635_display-custom-teminology --- cypress/e2e/ScopeSelector/ScopeSelector.js | 2 +- .../WidgetEnrollmentNote/index.js | 2 +- i18n/en.pot | 10 +- .../Enrollment/MissingMessage.component.tsx | 7 +- .../Enrollment/epics/fetchEnrollment.epics.ts | 15 ++- .../Relationships/Relationships.component.tsx | 9 +- .../Actions/AddNew/AddNew.component.tsx | 5 +- .../WidgetEnrollment.component.tsx | 17 ++- .../DeleteModal/DeleteModal.component.tsx | 6 +- .../WidgetProfile/hooks/useApiProgram.ts | 13 +- .../DeleteTeiAction/DeleteTeiAction.tsx | 5 +- .../metaData/helpers/customLabels.ts | 117 ++++++++++++++++++ .../helpers/customLabels/customLabels.ts | 51 -------- .../metaData/helpers/customLabels/index.ts | 5 - .../metaData/helpers/customLabels/tLabel.ts | 30 ----- .../metaData/helpers/customLabels/useLabel.ts | 58 --------- .../capture-core/metaData/helpers/index.ts | 3 - .../capture-core/metaData/index.ts | 3 - .../feedback.reducerDescriptionGetter.ts | 13 +- .../capture-core/utils/tCustomTerm.ts | 37 ++++++ 20 files changed, 212 insertions(+), 196 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/index.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts create mode 100644 src/core_modules/capture-core/utils/tCustomTerm.ts diff --git a/cypress/e2e/ScopeSelector/ScopeSelector.js b/cypress/e2e/ScopeSelector/ScopeSelector.js index b3298a75e6..90e7a8e300 100644 --- a/cypress/e2e/ScopeSelector/ScopeSelector.js +++ b/cypress/e2e/ScopeSelector/ScopeSelector.js @@ -284,7 +284,7 @@ And('you see the enrollment event Edit page but there is no org unit id in the u And('you see the enrollment event New page but there is no stage id in the url', () => { cy.url().should('eq', `${Cypress.config().baseUrl}/#/enrollmentEventNew?enrollmentId=Aemr3Q02aqV&orgUnitId=DiszpKrYNg8&programId=ur1Edk5Oe2n&teiId=eUTmQGull6H`); - cy.contains('Choose a stage for a new event'); + cy.contains('Choose a program stage for a new event'); }); And('you see the enrollment page without org unit in the url', () => { diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js index 67f9b5df5f..e96b83be28 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetEnrollmentNote/index.js @@ -3,7 +3,7 @@ import { When, Then } from '@badeball/cypress-cucumber-preprocessor'; const timeStamp = Math.round((new Date()).getTime() / 1000); Then('the stages and events should be loaded', () => { - cy.contains('Stages and Events').should('exist'); + cy.contains('Program stages and events').should('exist'); }); When(/^you fill in the note: (.*)$/, (note) => { diff --git a/i18n/en.pot b/i18n/en.pot index 3426f21610..61f6a6d9fa 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-17T07:58:18.382Z\n" -"PO-Revision-Date: 2026-08-17T07:58:18.383Z\n" +"POT-Creation-Date: 2026-08-28T14:48:19.309Z\n" +"PO-Revision-Date: 2026-08-28T14:48:19.309Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2242,9 +2242,15 @@ msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" msgid "enrollment" msgstr "enrollment" +msgid "enrollments" +msgstr "enrollments" + msgid "program stage" msgstr "program stage" +msgid "program stages" +msgstr "program stages" + msgid "note" msgstr "note" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index 49e1f3c7de..0a05eccfda 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -5,7 +5,8 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { useMissingCategoriesInProgramSelection } from '../../../hooks/useMissingCategoriesInProgramSelection'; import { scopeTypes } from '../../../metaData/helpers/constants'; -import { useTermLabel, tLabel } from '../../../metaData'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { enrollmentAccessLevels } from './EnrollmentPage.constants'; import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; @@ -302,8 +303,8 @@ const MissingMessagePlain = ({ missingStatus === missingStatuses.EVENT_PROGRAM_SELECTED &&
- {tLabel('{{programName}} is an event program and does not have {{enrollmentsLabel}}.', { - programName, enrollmentsLabel, interpolation: { escapeValue: false }, + {tCustomTerm('{{programName}} is an event program and does not have {{enrollmentsLabel}}.', { + programName, enrollmentsLabel, })}
{ } } const enrollmentsLabel = getTermLabel(programId, 'enrollment', { plural: true }); - const errorMessage = - tLabel('An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', { enrollmentsLabel }); + const errorMessage = tCustomTerm( + 'An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', + { enrollmentsLabel }, + ); return showErrorViewOnEnrollmentPage({ error: errorMessage }); }; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index 7b8ccb42e9..69c2596a9f 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -63,10 +63,9 @@ const styles: Readonly = (theme: any) => ({ }, }); -const getFromName = (entityType: string) => { - if (entityType === 'PROGRAM_STAGE_INSTANCE') return i18n.t('This event'); - return undefined; -}; +const getFromNames = () => ({ + PROGRAM_STAGE_INSTANCE: i18n.t('This event'), +}); type PlainProps = { relationships: Array; @@ -106,7 +105,7 @@ class RelationshipsPlain extends React.Component { const { onRenderConnectedEntity } = this.props; if (entity.id === this.props.currentEntityId) { - return getFromName(entity.type); + return getFromNames()[entity.type]; } return onRenderConnectedEntity ? onRenderConnectedEntity(entity) : entity.name; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx index 9b6e4ab68e..5ed79d8fbc 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/AddNew/AddNew.component.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { IconAdd16, MenuItem } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; -import { useTermLabel, tLabel } from '../../../../metaData'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import type { Props } from './addNew.types'; export const AddNew = ({ tetName, canAddNew, onlyEnrollOnce, onAddNew }: Props) => { @@ -14,7 +15,7 @@ export const AddNew = ({ tetName, canAddNew, onlyEnrollOnce, onAddNew }: Props) return ( setOpenStatus(false), [setOpenStatus])} open={open} > - {true && ( + {initError && (
- {tLabel('{{enrollmentLabel}} widget could not be loaded. Please try again later', - { enrollmentLabel })} + {tCustomTerm( + '{{enrollmentLabel}} widget could not be loaded. Please try again later', + { enrollmentLabel }, + )}
)} {loading && } diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx index f00449ad30..c7677de14b 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { Modal, ModalContent, ModalTitle, ModalActions, ButtonStrip, Button, NoticeBox } from '@dhis2/ui'; -import { useTermLabel, tLabel } from '../../../../../metaData'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import type { Props } from './DeleteModal.types'; import { useDeleteTrackedEntity } from './hooks'; import type { ErrorReport } from '../../processErrorReports'; @@ -25,11 +26,10 @@ export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, setOpenModal

{/* eslint-disable-next-line max-len */} - {tLabel('Are you sure you want to delete this {{trackedEntityTypeName}}? This will permanently remove the {{trackedEntityTypeName}} and all its associated {{enrollmentsLabel}} and events in all programs.', + {tCustomTerm('Are you sure you want to delete this {{trackedEntityTypeName}}? This will permanently remove the {{trackedEntityTypeName}} and all its associated {{enrollmentsLabel}} and events in all programs.', { trackedEntityTypeName, enrollmentsLabel, - interpolation: { escapeValue: false }, }, )}

diff --git a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts index 97fae559f4..ada6c9dde0 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts @@ -1,12 +1,7 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; -const trackedEntityTypeFields = - 'id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + - 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + - 'translations[property,locale,value]'; - -const buildFields = (): string => +const fields = 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + 'displayIncidentDate,access[*],' + @@ -28,7 +23,9 @@ const buildFields = (): string => 'displayDescription,valueType,optionSetValue,unique,orgunitScope,pattern,translations[property,locale,value],' + 'optionSet[id,displayName,version,valueType,options[id,displayName,name,code,style,translations]]],' + 'displayInList,searchable,mandatory,renderOptionsAsRadio,allowFutureDate],' + - `trackedEntityType[${trackedEntityTypeFields}],` + + 'trackedEntityType[id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + + 'translations[property,locale,value]],' + 'userRoles[id,displayName]'; export const useApiProgram = (programId: string) => { @@ -39,7 +36,7 @@ export const useApiProgram = (programId: string) => { resource: 'programs', id: programId, params: { - fields: buildFields(), + fields, }, }, }), diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx index 8205f6958d..f77db690d5 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx @@ -4,7 +4,8 @@ import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } fr import { useAuthority } from '../../../../../../utils/userInfo/useAuthority'; import { useCascadeDeleteTei } from './hooks/useCascadeDeleteTei'; import type { PlainProps } from './DeleteTeiAction.types'; -import { useTermLabel, tLabel } from '../../../../../../metaData'; +import { useTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const CASCADE_DELETE_TEI_AUTHORITY = 'F_TEI_CASCADE_DELETE'; @@ -35,7 +36,7 @@ export const DeleteTeiAction = ({ small onClick={() => setIsDeleteDialogOpen(true)} > - {tLabel('Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}', { + {tCustomTerm('Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}', { trackedEntityName: trackedEntityName.toLowerCase(), enrollmentsLabel, })} diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts new file mode 100644 index 0000000000..aa7964603b --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -0,0 +1,117 @@ +import i18n from '@dhis2/d2-i18n'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { programCollection } from '../../metaDataMemoryStores'; + +type LabelConfig = { + field: string; + pluralField?: string; + singular: () => string; + plural?: () => string; +}; + +const asLabels = (labels: Record) => labels; + +const LABELS = asLabels({ + enrollment: { + field: 'displayEnrollmentLabel', + pluralField: 'displayEnrollmentsLabel', + singular: () => i18n.t('enrollment'), + plural: () => i18n.t('enrollments'), + }, + event: { + field: 'displayEventLabel', + pluralField: 'displayEventsLabel', + singular: () => i18n.t('event'), + plural: () => i18n.t('events'), + }, + programStage: { + field: 'displayProgramStageLabel', + pluralField: 'displayProgramStagesLabel', + singular: () => i18n.t('program stage'), + plural: () => i18n.t('program stages'), + }, + note: { + field: 'displayNoteLabel', + singular: () => i18n.t('note'), + }, + relationship: { + field: 'displayRelationshipLabel', + singular: () => i18n.t('relationship'), + }, + attribute: { + field: 'displayTrackedEntityAttributeLabel', + singular: () => i18n.t('attribute'), + }, + orgUnit: { + field: 'displayOrgUnitLabel', + singular: () => i18n.t('organisation unit'), + }, + followUp: { + field: 'displayFollowUpLabel', + singular: () => i18n.t('follow-up'), + }, +}); + +export type CustomLabelKey = keyof typeof LABELS; +export type CustomLabels = Record; +export type LabelOptions = { plural?: boolean }; + +const ALL_FIELD_NAMES = Object.values(LABELS).flatMap( + ({ field, pluralField }) => (pluralField ? [field, pluralField] : [field]), +); + +export const extractCustomLabels = (cached: Record): CustomLabels => + Object.fromEntries( + ALL_FIELD_NAMES + .filter(field => typeof cached[field] === 'string') + .map(field => [field, cached[field] as string]), + ); + +type LabelSource = CustomLabels | undefined | null; + +export const resolveLabel = ( + sources: LabelSource | Array, + key: CustomLabelKey, + { plural = false }: LabelOptions = {}, +): string | undefined => { + const { field, pluralField } = LABELS[key]; + const target = plural ? pluralField : field; + if (!target) return undefined; + const list = Array.isArray(sources) ? sources : [sources]; + return list.find(source => source?.[target])?.[target]; +}; + +type TermLabelOptions = LabelOptions & { stageId?: string; programId?: string }; + +const resolveTerm = ( + programId: string | undefined, + key: CustomLabelKey, + { stageId, plural = false }: TermLabelOptions, +): string => { + const program = programId ? programCollection.get(programId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + const custom = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); + if (custom) return custom; + if (plural) return LABELS[key].plural?.() ?? `${key}s`; + return LABELS[key].singular(); +}; + +export const getTermLabel = ( + programId: string | undefined, + key: CustomLabelKey, + options: TermLabelOptions = {}, +): string => resolveTerm(programId, key, options); + +export const useTermLabel = ( + key: CustomLabelKey, + options: TermLabelOptions = {}, +): string => { + const { programId, stageId, plural } = options; + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const id = programId ?? currentProgramId; + return useMemo( + () => resolveTerm(id, key, { stageId, plural }), + [id, key, stageId, plural], + ); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts deleted file mode 100644 index bf60ed5c72..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ /dev/null @@ -1,51 +0,0 @@ -type CustomLabelField = { - field?: string, - pluralField?: string, -}; - -export const CUSTOM_LABEL_FIELDS = { - enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - note: { field: 'displayNoteLabel' }, - relationship: { field: 'displayRelationshipLabel' }, - attribute: { field: 'displayTrackedEntityAttributeLabel' }, - orgUnit: { field: 'displayOrgUnitLabel' }, - followUp: { field: 'displayFollowUpLabel' }, -} as const satisfies { [key: string]: CustomLabelField }; - -export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; -export type CustomLabels = Record; -export type LabelOptions = { plural?: boolean }; - -const allFields: Array = Array.from( - new Set( - Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) - .filter((field): field is string => Boolean(field)), - ), -); - -export const extractCustomLabels = (cached: Record): CustomLabels => { - const labels: CustomLabels = {}; - allFields.forEach((field) => { - if (cached[field]) labels[field] = cached[field]; - }); - return labels; -}; - -type LabelSource = CustomLabels | undefined | null; - -export const resolveLabel = ( - sources: LabelSource | Array, - key: CustomLabelKey, - { plural = false }: LabelOptions = {}, -): string | undefined => { - const term = CUSTOM_LABEL_FIELDS[key] as CustomLabelField; - const list = Array.isArray(sources) ? sources : [sources]; - const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); - if (plural && term.pluralField) { - return pick(term.pluralField); - } - return pick(term.field); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts deleted file mode 100644 index 9c9de5821a..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel } from './customLabels'; -export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; -export { getTermLabel, useTermLabel } from './useLabel'; -export { tLabel } from './tLabel'; -export { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts deleted file mode 100644 index 172b02b9c5..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/tLabel.ts +++ /dev/null @@ -1,30 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; - -const getRawTranslation = (key: string): string => - (i18n as any).getResource((i18n as any).language, 'default', key) - ?? (i18n as any).getResource('en', 'default', key) - ?? key; - -const startsWithVar = (raw: string, varName: string): boolean => { - const trimmed = raw.trimStart(); - return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); -}; - -/** - * Drop-in replacement for i18n.t() when the string contains custom label variables. - * Automatically capitalizes a variable's value when it appears as the first word - * in the translated string — without requiring any changes to translation files. - */ -export const tLabel = (key: string, options: Record = {}): string => { - const raw = getRawTranslation(key); - const processedOptions = { ...options }; - - for (const [varName, value] of Object.entries(options)) { - if (typeof value === 'string' && startsWithVar(raw, varName)) { - processedOptions[varName] = capitalizeFirstLetter(value); - } - } - - return i18n.t(key, { ...processedOptions, interpolation: { escapeValue: false } }); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts deleted file mode 100644 index dae529d57f..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ /dev/null @@ -1,58 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { programCollection } from '../../../metaDataMemoryStores'; -import { resolveLabel } from './customLabels'; -import type { CustomLabelKey, LabelOptions } from './customLabels'; - -const defaults: Record string> = { - enrollment: () => i18n.t('enrollment'), - event: () => i18n.t('event'), - programStage: () => i18n.t('program stage'), - note: () => i18n.t('note'), - relationship: () => i18n.t('relationship'), - attribute: () => i18n.t('attribute'), - orgUnit: () => i18n.t('organisation unit'), - followUp: () => i18n.t('follow-up'), -}; - -type TermLabelOptions = LabelOptions & { stageId?: string }; - -const resolve = ( - programId: string | undefined, - key: CustomLabelKey, - { stageId, plural }: TermLabelOptions, -): string => { - const program = programId ? programCollection.get(programId) : undefined; - const stage = program && stageId ? program.getStage(stageId) : undefined; - const customLabel = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); - return customLabel ?? defaults[key](); -}; - -/** - * Works anywhere — components, reducers, epics. - * Returns the custom label from the program (or stage), falling back to the - * translated default term. - */ -export const getTermLabel = ( - programId: string | undefined, - key: CustomLabelKey, - options: TermLabelOptions = {}, -): string => resolve(programId, key, options); - -/** - * React hook version — reads programId from Redux automatically. - * Pass programId explicitly to override (e.g. cross-program widgets). - */ -export const useTermLabel = ( - key: CustomLabelKey, - options: TermLabelOptions & { programId?: string } = {}, -): string => { - const { programId, stageId, plural } = options; - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const id = programId ?? currentProgramId; - return useMemo( - () => resolve(id, key, { stageId, plural }), - [id, key, stageId, plural], - ); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index 94a88e45ee..b4158ea1a9 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -18,12 +18,9 @@ export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { - CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, useTermLabel, - tLabel, - capitalizeFirstLetter, } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index a098e321b4..1e8e7461b6 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -40,12 +40,9 @@ export { getProgramThrowIfNotFound, getProgramAndStageForEventProgram, getEventProgramEventAccess, - CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel, getTermLabel, useTermLabel, - tLabel, - capitalizeFirstLetter, } from './helpers'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts index dc816ccd1b..9b3b3e93a0 100644 --- a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts +++ b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts @@ -5,7 +5,8 @@ import isString from 'd2-utilizr/lib/isString'; import isObject from 'd2-utilizr/lib/isObject'; import uuid from 'd2-utilizr/lib/uuid'; import { errorCreator } from 'capture-core-utils'; -import { getTermLabel, tLabel } from '../../metaData'; +import { getTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import { createReducerDescription } from '../../trackerRedux/trackerReducer'; import { actionTypes as feedbackActionTypes } from '../../components/FeedbackBar/actions/feedback.actions'; import { actionTypes as dataEntryActionTypes } from '../../components/DataEntry/actions/dataEntry.actions'; @@ -112,15 +113,17 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback({ message: i18n.t('Error saving tracked entity instance') }), [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: () => { const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tLabel('Error saving {{enrollmentLabel}}', { enrollmentLabel }) }); + return addErrorFeedback({ message: tCustomTerm('Error saving {{enrollmentLabel}}', { enrollmentLabel }) }); }, [enrollmentSiteActionTypes.SAVE_FAILED]: () => { const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tLabel('Error saving the {{enrollmentLabel}} event', { enrollmentLabel }) }); + return addErrorFeedback({ message: tCustomTerm('Error saving the {{enrollmentLabel}} event', { enrollmentLabel }) }); }, [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: () => { const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tLabel('Error deleting the {{enrollmentLabel}} event', { enrollmentLabel }) }); + return addErrorFeedback({ + message: tCustomTerm('Error deleting the {{enrollmentLabel}} event', { enrollmentLabel }), + }); }, [editEventDataEntryAction.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED]: () => addErrorFeedback({ message: i18n.t('Error editing the event, the changes made were not saved') }), @@ -132,7 +135,7 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: () => { const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tLabel('Could not save {{enrollmentLabel}} note', { enrollmentLabel }) }); + return addErrorFeedback({ message: tCustomTerm('Could not save {{enrollmentLabel}} note', { enrollmentLabel }) }); }, [eventNoteActionTypes.ADD_NOTE_FAILED_FOR_EVENT]: () => addErrorFeedback({ message: i18n.t('Could not save event note') }), diff --git a/src/core_modules/capture-core/utils/tCustomTerm.ts b/src/core_modules/capture-core/utils/tCustomTerm.ts new file mode 100644 index 0000000000..dcef43f076 --- /dev/null +++ b/src/core_modules/capture-core/utils/tCustomTerm.ts @@ -0,0 +1,37 @@ +import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from './capitalizeFirstLetter'; + +type I18nInternal = { + getResource: (lang: string, ns: string, key: string) => string | undefined; + language: string; +}; +const internal = i18n as unknown as I18nInternal; + +const getRawTranslation = (key: string): string => + internal.getResource(internal.language, 'default', key) + ?? internal.getResource('en', 'default', key) + ?? key; + +const startsWithVar = (raw: string, varName: string): boolean => { + const trimmed = raw.trimStart(); + return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); +}; + +export const tCustomTerm = (key: string, options: Record = {}): string => { + const raw = getRawTranslation(key); + const { interpolation, ...values } = options; + + const cased = Object.fromEntries( + Object.entries(values).map(([name, value]) => [ + name, + typeof value === 'string' && startsWithVar(raw, name) + ? capitalizeFirstLetter(value) + : value, + ]), + ); + + return i18n.t(key, { + ...cased, + interpolation: { escapeValue: false, ...(interpolation as Record ?? {}) }, + }); +}; From 0bd1e280ae272ce6a1cd563f178e09a43f597bf9 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:02:35 +0000 Subject: [PATCH 035/118] feat: add plural support for custom labels in note, relationship, and attribute --- i18n/en.pot | 31 +++++++------------ .../metaData/helpers/customLabels.ts | 6 ++++ 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a14c266b6e..a1b8aa4452 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-28T14:51:05.211Z\n" -"PO-Revision-Date: 2026-08-28T14:51:05.211Z\n" +"POT-Creation-Date: 2026-08-31T09:02:36.773Z\n" +"PO-Revision-Date: 2026-08-31T09:02:36.773Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -196,9 +196,6 @@ msgstr "Assigned user" msgid "Search for user" msgstr "Search for user" -msgid "Notes" -msgstr "Notes" - msgid "Basic info" msgstr "Basic info" @@ -979,9 +976,6 @@ msgstr "" msgid "Errors" msgstr "Errors" -msgid "This event doesn't have any notes" -msgstr "This event doesn't have any notes" - msgid "This event doesn't have any relationships" msgstr "This event doesn't have any relationships" @@ -1467,15 +1461,9 @@ msgstr "Saving to {{stageName}} for {{programName}}" msgid "Program or program stage is invalid" msgstr "Program or program stage is invalid" -msgid "Notes about this {{enrollmentLabel}}" -msgstr "Notes about this {{enrollmentLabel}}" - msgid "Write a note about this {{enrollmentLabel}}" msgstr "Write a note about this {{enrollmentLabel}}" -msgid "This {{enrollmentLabel}} doesn't have any notes" -msgstr "This {{enrollmentLabel}} doesn't have any notes" - msgid "Error" msgstr "Error" @@ -1521,9 +1509,6 @@ msgstr "No polygon captured" msgid "Event completed" msgstr "Event completed" -msgid "Notes about this event" -msgstr "Notes about this event" - msgid "Write a note about this event" msgstr "Write a note about this event" @@ -1572,9 +1557,6 @@ msgstr "Scheduling an event in {{stageName}} for {{programName}}" msgid "Schedule info" msgstr "Schedule info" -msgid "Event notes" -msgstr "Event notes" - msgid "Write a note about this scheduled event" msgstr "Write a note about this scheduled event" @@ -2254,12 +2236,21 @@ msgstr "program stages" msgid "note" msgstr "note" +msgid "notes" +msgstr "notes" + msgid "relationship" msgstr "relationship" +msgid "relationships" +msgstr "relationships" + msgid "attribute" msgstr "attribute" +msgid "attributes" +msgstr "attributes" + msgid "organisation unit" msgstr "organisation unit" diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts index aa7964603b..cd86feead8 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -33,15 +33,21 @@ const LABELS = asLabels({ }, note: { field: 'displayNoteLabel', + pluralField: 'displayNotesLabel', singular: () => i18n.t('note'), + plural: () => i18n.t('notes'), }, relationship: { field: 'displayRelationshipLabel', + pluralField: 'displayRelationshipsLabel', singular: () => i18n.t('relationship'), + plural: () => i18n.t('relationships'), }, attribute: { field: 'displayTrackedEntityAttributeLabel', + pluralField: 'displayTrackedEntityAttributesLabel', singular: () => i18n.t('attribute'), + plural: () => i18n.t('attributes'), }, orgUnit: { field: 'displayOrgUnitLabel', From 2c132abb8ce384ba94d03fcc97af35be58f8e029 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:09:37 +0000 Subject: [PATCH 036/118] feat: implement custom plural terminology for notes across components --- i18n/en.pot | 4 ++-- .../DataEntry/DataEntry.component.tsx | 11 ++++++----- .../DataEntryWrapper/DataEntry/DataEntry.container.ts | 9 ++++++++- .../NotesSection/NotesSection.component.tsx | 10 +++++----- .../NotesSection/NotesSection.container.tsx | 9 ++++++++- .../RightColumn/NotesSection/NotesSection.types.ts | 1 + .../DataEntry/DataEntry.component.tsx | 11 ++++++----- .../DataEntry/DataEntry.container.tsx | 9 ++++++++- .../WidgetEnrollmentNote.component.tsx | 9 +++++++-- .../ViewEventDataEntry.component.tsx | 9 +++++---- .../ViewEventDataEntry.container.ts | 8 +++++++- .../WidgetEventNote/WidgetEventNote.component.tsx | 9 ++++++--- .../WidgetEventSchedule.component.tsx | 7 +++++-- 13 files changed, 74 insertions(+), 32 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a1b8aa4452..5762571408 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T09:02:36.773Z\n" -"PO-Revision-Date: 2026-08-31T09:02:36.773Z\n" +"POT-Creation-Date: 2026-08-31T09:09:39.263Z\n" +"PO-Revision-Date: 2026-08-31T09:09:39.263Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index a60123b6fe..9a8f1f871a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -402,7 +402,7 @@ const buildNotesSettingsFn = () => { const notesSettings = { getComponent: () => noteComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Notes'), + label: props.notesLabel, onAddNote: props.onAddNote, id: 'notes', dataEntryId: props.id, @@ -541,13 +541,14 @@ type Props = { formHorizontal: boolean | null, recentlyAddedRelationshipId?: string | null, onScrollToRelationships: () => void; + notesLabel: string, }; type DataEntrySection = { placement: typeof placements[keyof typeof placements], name?: string, }; -const dataEntrySectionDefinitions = { +const buildDataEntrySectionDefinitions = (notesLabel: string) => ({ [dataEntrySectionNames.BASICINFO]: { placement: placements.TOP, name: i18n.t('Basic info'), @@ -561,7 +562,7 @@ const dataEntrySectionDefinitions = { }, [dataEntrySectionNames.NOTES]: { placement: placements.BOTTOM, - name: i18n.t('Notes'), + name: notesLabel, }, [dataEntrySectionNames.RELATIONSHIPS]: { placement: placements.BOTTOM, @@ -571,7 +572,7 @@ const dataEntrySectionDefinitions = { placement: placements.BOTTOM, name: i18n.t('Assignee'), }, -}; +}); class NewEventDataEntry extends Component> { fieldOptions: { theme: any }; @@ -581,7 +582,7 @@ class NewEventDataEntry extends Component> this.fieldOptions = { theme: props.theme, }; - this.dataEntrySections = dataEntrySectionDefinitions; + this.dataEntrySections = buildDataEntrySectionDefinitions(props.notesLabel); } componentDidMount() { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts index 6248331077..167e47416a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts @@ -5,6 +5,7 @@ import { batchActions } from 'redux-batched-actions'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import type { ReduxAction } from 'capture-core-utils/types'; import { DataEntryComponent } from './DataEntry.component'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; import { startRunRulesPostUpdateField } from '../../../../DataEntry'; import { startAsyncUpdateFieldForNewEvent, @@ -26,6 +27,10 @@ import type { RenderFoundation } from '../../../../../metaData'; import { withLoadingIndicator, withErrorMessageHandler } from '../../../../../HOC'; import { newEventSaveTypes } from './newEventSaveTypes'; +const customLabels = { + notesLabel: { key: 'note', plural: true }, +} as const; + const makeMapStateToProps = () => { const programNameSelector = makeProgramNameSelector(); @@ -111,5 +116,7 @@ const mapDispatchToProps = (dispatch: any) => ({ }); export const DataEntry = connect(makeMapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(withErrorMessageHandler()(DataEntryComponent)), + withLoadingIndicator()(withErrorMessageHandler()( + withCustomLabels(customLabels)(DataEntryComponent), + )), ); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx index aba080dd1b..7b67d7c11d 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { IconMessages24, colors, spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; @@ -8,6 +7,7 @@ import { ViewEventSection } from '../../Section/ViewEventSection.component'; import { ViewEventSectionHeader } from '../../Section/ViewEventSectionHeader.component'; import { Notes } from '../../../../Notes/Notes.component'; import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import type { PlainProps } from './NotesSection.types'; const LoadingNotes = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Notes); @@ -34,13 +34,13 @@ type Props = PlainProps & WithStyles; class NotesSectionPlain extends React.Component { renderHeader = () => { - const { classes, notes, ready } = this.props; + const { classes, notes, ready, notesLabel } = this.props; const count = notes ? notes.length : 0; const badgeCount = ready ? count : undefined; return ( @@ -48,7 +48,7 @@ class NotesSectionPlain extends React.Component { } render() { - const { classes, notes, fieldValue, onAddNote, ready, readOnly } = this.props; + const { classes, notes, fieldValue, onAddNote, ready, readOnly, notesLabel } = this.props; const isEmpty = ready && (!notes || notes.length === 0); return ( { > {isEmpty && (
- {i18n.t("This event doesn't have any notes")} + {tCustomTerm("This event doesn't have any {{notesLabel}}", { notesLabel })}
)} {React.createElement(LoadingNotes as any, { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx index b783a89832..3e89c8153a 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx @@ -1,6 +1,11 @@ import { connect } from 'react-redux'; import { NotesSectionComponent } from './NotesSection.component'; import { requestSaveEventNote, updateEventNoteField } from '../../Notes/viewEventNotes.actions'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; + +const customLabels = { + notesLabel: { key: 'note', plural: true }, +} as const; const mapStateToProps = (state: any) => { const notesSection = state.viewEventPage.notesSection || {}; @@ -20,4 +25,6 @@ const mapDispatchToProps = (dispatch: any) => ({ }, }); -export const NotesSection = connect(mapStateToProps, mapDispatchToProps)(NotesSectionComponent); +export const NotesSection = connect(mapStateToProps, mapDispatchToProps)( + withCustomLabels(customLabels)(NotesSectionComponent), +); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts index 2a4bc4e5e4..388ff4a913 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts @@ -5,4 +5,5 @@ export type PlainProps = { fieldValue?: string; ready: boolean; readOnly: boolean; + notesLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx index 411a31e2e6..8efcfc5148 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx @@ -308,7 +308,7 @@ const buildNotesSettingsFn = () => { const notesSettings = { getComponent: () => noteComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Notes'), + label: props.notesLabel, onAddNote: props.onAddNote, id: 'notes', dataEntryId: props.id, @@ -441,6 +441,7 @@ type Props = { placementDomNodeForSavingText?: HTMLElement; programName: string; orgUnitFieldValue?: OrgUnit | null; + notesLabel: string; }; type DataEntrySection = { @@ -448,7 +449,7 @@ type DataEntrySection = { name: string, }; -const dataEntrySectionDefinitions = { +const buildDataEntrySectionDefinitions = (notesLabel: string) => ({ [dataEntrySectionNames.BASICINFO]: { placement: placements.TOP, name: i18n.t('Basic info'), @@ -459,7 +460,7 @@ const dataEntrySectionDefinitions = { }, [dataEntrySectionNames.NOTES]: { placement: placements.BOTTOM, - name: i18n.t('Notes'), + name: notesLabel, }, [dataEntrySectionNames.RELATIONSHIPS]: { placement: placements.BOTTOM, @@ -473,7 +474,7 @@ const dataEntrySectionDefinitions = { placement: placements.TOP, name: '', }, -}; +}); class DataEntryPlain extends Component> { relationshipsInstance?: HTMLDivElement | null; dataEntrySections: { [key: string]: DataEntrySection }; @@ -484,7 +485,7 @@ class DataEntryPlain extends Component> { theme: props.theme, fieldLabelMediaBasedClass: props.classes.fieldLabelMediaBased, }; - this.dataEntrySections = dataEntrySectionDefinitions; + this.dataEntrySections = buildDataEntrySectionDefinitions(props.notesLabel); } componentDidMount() { diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx index 5b1f9e9a42..b2e71221e7 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx @@ -15,6 +15,13 @@ import { import type { AddEventSaveType } from './addEventSaveTypes'; import type { ContainerProps } from './dataEntry.types'; import { useProgramExpiryForUser } from '../../../hooks'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; + +const customLabels = { + notesLabel: { key: 'note', plural: true }, +} as const; + +const WrappedDataEntryComponent = withCustomLabels(customLabels)(DataEntryComponent); export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...passOnProps }: ContainerProps) => { const dispatch = useDispatch(); @@ -77,7 +84,7 @@ export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...pa dispatch(setNewEventSaveTypes(newSaveTypes)); }, [dispatch]); return ( - { const dispatch = useDispatch(); @@ -19,6 +20,7 @@ export const WidgetEnrollmentNote = () => { showWidgetBadge, } = useEnrollmentAccessContext(); const enrollmentLabel = useTermLabel('enrollment'); + const notesLabel = useTermLabel('note', { plural: true }); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); @@ -27,9 +29,12 @@ export const WidgetEnrollmentNote = () => { return (
({ [dataEntrySectionNames.BASICINFO]: { placement: placements.TOP, name: i18n.t('Basic info'), @@ -301,13 +302,13 @@ const dataEntrySectionDefinitions = { }, [dataEntrySectionNames.NOTES]: { placement: placements.BOTTOM, - name: i18n.t('Notes'), + name: notesLabel, }, [AOCsectionKey]: { placement: placements.TOP, name: '', }, -}; +}); class ViewEventDataEntryPlain extends Component> { fieldOptions: { theme: any; fieldLabelMediaBasedClass: string }; @@ -319,7 +320,7 @@ class ViewEventDataEntryPlain extends Component { const eventDetailsSection = state.viewEventPage.eventDetailsSection || {}; @@ -20,5 +24,7 @@ const mapStateToProps = (state: any, props: any) => { const mapDispatchToProps = (): any => ({}); export const ViewEventDataEntry = connect(mapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(ViewEventDataEntryComponent), + withLoadingIndicator()( + withCustomLabels(customLabels)(ViewEventDataEntryComponent), + ), ); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index d2306069a7..029273b97f 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import i18n from '@dhis2/d2-i18n'; +import { useDispatch, useSelector } from 'react-redux'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); @@ -16,6 +18,7 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { trackedEntityTypeName, showWidgetBadge, } = useEnrollmentAccessContext(); + const notesLabel = useTermLabel('note', { plural: true }); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue)); @@ -24,9 +27,9 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { return (
({ wrapper: { @@ -57,6 +59,7 @@ const WidgetEventSchedulePlain = ({ setValidation, ...passOnProps }: Props & WithStyles) => { + const notesLabel = useTermLabel('note', { plural: true, programId }); const onSelectOrgUnit = (e: { id: string; displayName: string; path: string }) => { setScheduledOrgUnit({ id: e.id, @@ -121,12 +124,12 @@ const WidgetEventSchedulePlain = ({ } From cb9b19595a8243ba0b613e018592be4025793d3f Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:17:38 +0000 Subject: [PATCH 037/118] feat: consolidate capitalizeFirstLetter utility and update imports --- i18n/en.pot | 4 +-- .../string/capitalizeFirstLetter.ts | 12 +++++-- .../capture-core/HOC/withCustomLabels.tsx | 26 +++++++++++++++ .../WidgetEnrollment.component.tsx | 2 +- .../utils/capitalizeFirstLetter.ts | 11 ------- .../capture-core/utils/tCustomTerm.ts | 32 +++++++++---------- 6 files changed, 54 insertions(+), 33 deletions(-) create mode 100644 src/core_modules/capture-core/HOC/withCustomLabels.tsx delete mode 100644 src/core_modules/capture-core/utils/capitalizeFirstLetter.ts diff --git a/i18n/en.pot b/i18n/en.pot index bcfc414f2d..70ea1516ce 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-28T14:37:10.874Z\n" -"PO-Revision-Date: 2026-08-28T14:37:10.874Z\n" +"POT-Creation-Date: 2026-08-31T09:17:40.273Z\n" +"PO-Revision-Date: 2026-08-31T09:17:40.273Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts b/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts index 0184db6f88..fdbe04456c 100644 --- a/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts +++ b/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts @@ -1,5 +1,11 @@ +import i18n from '@dhis2/d2-i18n'; + export function capitalizeFirstLetter(text: string) { - const first = text.charAt(0).toLocaleUpperCase(); - const rest = text.slice(1); - return first + rest; + if (!text) return text; + const locale = (i18n as any).language ?? 'en'; + try { + return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1); + } catch { + return text.charAt(0).toUpperCase() + text.slice(1); + } } diff --git a/src/core_modules/capture-core/HOC/withCustomLabels.tsx b/src/core_modules/capture-core/HOC/withCustomLabels.tsx new file mode 100644 index 0000000000..659b0957ad --- /dev/null +++ b/src/core_modules/capture-core/HOC/withCustomLabels.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; +import { useTermLabel } from '../metaData'; +import type { CustomLabelKey } from '../metaData/helpers/customLabels'; + +type LabelSpec = { + key: CustomLabelKey; + plural?: boolean; +}; + +type LabelSpecs = Record; + +type InjectedLabels = { [K in keyof S]: string }; + +export const withCustomLabels = + (specs: S) => +

>(WrappedComponent: React.ComponentType

>) => + (props: P) => { + const labels = Object.fromEntries( + Object.entries(specs).map(([propName, { key, plural }]) => [ + propName, + capitalizeFirstLetter(useTermLabel(key, { plural })), + ]), + ) as InjectedLabels; + return React.createElement(WrappedComponent, { ...props, ...labels }); + }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 2949fd4e17..43f61a5a59 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -11,6 +11,7 @@ import { import i18n from '@dhis2/d2-i18n'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; @@ -19,7 +20,6 @@ import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes, useTermLabel } from '../../metaData'; import { tCustomTerm } from '../../utils/tCustomTerm'; -import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'; import { convertValue } from '../../converters/clientToView'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; diff --git a/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts b/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts deleted file mode 100644 index cf3370402a..0000000000 --- a/src/core_modules/capture-core/utils/capitalizeFirstLetter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; - -export const capitalizeFirstLetter = (str: string): string => { - if (!str) return str; - const locale = (i18n as any).language ?? 'en'; - try { - return str.charAt(0).toLocaleUpperCase(locale) + str.slice(1); - } catch { - return str.charAt(0).toUpperCase() + str.slice(1); - } -}; diff --git a/src/core_modules/capture-core/utils/tCustomTerm.ts b/src/core_modules/capture-core/utils/tCustomTerm.ts index dcef43f076..1e9ceb81c0 100644 --- a/src/core_modules/capture-core/utils/tCustomTerm.ts +++ b/src/core_modules/capture-core/utils/tCustomTerm.ts @@ -1,5 +1,5 @@ import i18n from '@dhis2/d2-i18n'; -import { capitalizeFirstLetter } from './capitalizeFirstLetter'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; type I18nInternal = { getResource: (lang: string, ns: string, key: string) => string | undefined; @@ -7,31 +7,31 @@ type I18nInternal = { }; const internal = i18n as unknown as I18nInternal; -const getRawTranslation = (key: string): string => +const getTranslatedTemplate = (key: string): string => internal.getResource(internal.language, 'default', key) ?? internal.getResource('en', 'default', key) ?? key; -const startsWithVar = (raw: string, varName: string): boolean => { - const trimmed = raw.trimStart(); - return trimmed.startsWith(`{{${varName}}}`) || trimmed.startsWith(`{{${varName},`); +type Options = { + interpolation?: Record; + [key: string]: unknown; }; -export const tCustomTerm = (key: string, options: Record = {}): string => { - const raw = getRawTranslation(key); +export const tCustomTerm = (key: string, options: Options = {}): string => { const { interpolation, ...values } = options; + const template = getTranslatedTemplate(key).trimStart(); - const cased = Object.fromEntries( - Object.entries(values).map(([name, value]) => [ - name, - typeof value === 'string' && startsWithVar(raw, name) - ? capitalizeFirstLetter(value) - : value, - ]), + const casedValues = Object.fromEntries( + Object.entries(values).map(([name, value]) => { + const variableIsAtSentenceStart = template.startsWith(`{{${name}}}`) + || template.startsWith(`{{${name},`); + const shouldCapitalize = typeof value === 'string' && variableIsAtSentenceStart; + return [name, shouldCapitalize ? capitalizeFirstLetter(value) : value]; + }), ); return i18n.t(key, { - ...cased, - interpolation: { escapeValue: false, ...(interpolation as Record ?? {}) }, + ...casedValues, + interpolation: { escapeValue: false, ...(interpolation ?? {}) }, }); }; From 3ac34d417100330d3e1bc86a7152e29061058b28 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:36:30 +0000 Subject: [PATCH 038/118] feat: relationships labels plural --- i18n/en.pot | 22 ++----------------- .../DataEntry/DataEntry.component.tsx | 7 +++--- .../DataEntry/DataEntry.container.ts | 1 + .../RelationshipsSection.component.tsx | 10 ++++----- .../RelationshipsSection.container.tsx | 9 +++++++- .../RelationshipsSection.types.ts | 1 + .../DataEntry/DataEntry.component.tsx | 7 +++--- .../DataEntry/DataEntry.container.tsx | 1 + .../RelatedStagesActions.component.tsx | 8 ++++++- ...getTrackedEntityRelationship.component.tsx | 13 +++++++---- .../RelationshipsWidget.component.tsx | 8 +++++-- 11 files changed, 48 insertions(+), 39 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 8a4bc41352..05bffb2275 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T09:24:37.476Z\n" -"PO-Revision-Date: 2026-08-31T09:24:37.476Z\n" +"POT-Creation-Date: 2026-08-31T09:36:32.334Z\n" +"PO-Revision-Date: 2026-08-31T09:36:32.335Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -202,9 +202,6 @@ msgstr "Basic info" msgid "Status" msgstr "Status" -msgid "Relationships" -msgstr "Relationships" - msgid "Assignee" msgstr "Assignee" @@ -976,9 +973,6 @@ msgstr "" msgid "Errors" msgstr "Errors" -msgid "This event doesn't have any relationships" -msgstr "This event doesn't have any relationships" - msgid "Warnings" msgstr "Warnings" @@ -1680,9 +1674,6 @@ msgstr "{{ linkableStageLabel }} has no linkable events" msgid "Actions - {{relationshipName}}" msgstr "Actions - {{relationshipName}}" -msgid "Ambiguous relationships, contact system administrator" -msgstr "Ambiguous relationships, contact system administrator" - msgid "Enter details" msgstr "Enter details" @@ -1882,12 +1873,6 @@ msgstr "Link to an existing {{tetName}}" msgid "An error occurred while adding the relationship" msgstr "An error occurred while adding the relationship" -msgid "Something went wrong while loading relationships. Please try again later." -msgstr "Something went wrong while loading relationships. Please try again later." - -msgid "{{trackedEntityTypeName}} relationships" -msgstr "{{trackedEntityTypeName}} relationships" - msgid "Delete relationship" msgstr "Delete relationship" @@ -1906,9 +1891,6 @@ msgstr "An error occurred while deleting the relationship." msgid "To open this relationship, please wait until saving is complete" msgstr "To open this relationship, please wait until saving is complete" -msgid "This {{enrollmentLabel}} doesn't have any relationships" -msgstr "This {{enrollmentLabel}} doesn't have any relationships" - msgid "Type" msgstr "Type" diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index 9a8f1f871a..b7f92ae2a2 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -542,13 +542,14 @@ type Props = { recentlyAddedRelationshipId?: string | null, onScrollToRelationships: () => void; notesLabel: string, + relationshipsLabel: string, }; type DataEntrySection = { placement: typeof placements[keyof typeof placements], name?: string, }; -const buildDataEntrySectionDefinitions = (notesLabel: string) => ({ +const buildDataEntrySectionDefinitions = (notesLabel: string, relationshipsLabel: string) => ({ [dataEntrySectionNames.BASICINFO]: { placement: placements.TOP, name: i18n.t('Basic info'), @@ -566,7 +567,7 @@ const buildDataEntrySectionDefinitions = (notesLabel: string) => ({ }, [dataEntrySectionNames.RELATIONSHIPS]: { placement: placements.BOTTOM, - name: i18n.t('Relationships'), + name: relationshipsLabel, }, [dataEntrySectionNames.ASSIGNEE]: { placement: placements.BOTTOM, @@ -582,7 +583,7 @@ class NewEventDataEntry extends Component> this.fieldOptions = { theme: props.theme, }; - this.dataEntrySections = buildDataEntrySectionDefinitions(props.notesLabel); + this.dataEntrySections = buildDataEntrySectionDefinitions(props.notesLabel, props.relationshipsLabel); } componentDidMount() { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts index 167e47416a..1a1186f56b 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts @@ -29,6 +29,7 @@ import { newEventSaveTypes } from './newEventSaveTypes'; const customLabels = { notesLabel: { key: 'note', plural: true }, + relationshipsLabel: { key: 'relationship', plural: true }, } as const; const makeMapStateToProps = () => { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx index b9f3302044..78f714b5d9 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { IconLink24, colors, spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; @@ -8,6 +7,7 @@ import { ViewEventSection } from '../../Section/ViewEventSection.component'; import { ViewEventSectionHeader } from '../../Section/ViewEventSectionHeader.component'; import { Relationships } from '../../../../Relationships/Relationships.component'; import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import { ConnectedEntity } from './ConnectedEntity'; import type { Entity } from '../../../../Relationships/relationships.types'; import type { PlainProps } from './RelationshipsSection.types'; @@ -45,13 +45,13 @@ class RelationshipsSectionPlain extends React.Component { } renderHeader = () => { - const { classes, relationships, ready } = this.props; + const { classes, relationships, ready, relationshipsLabel } = this.props; const count = relationships ? relationships.length : 0; const badgeCount = ready ? count : undefined; return ( @@ -76,7 +76,7 @@ class RelationshipsSectionPlain extends React.Component { } render() { - const { classes, programStage, eventId, relationships, ready, readOnly } = this.props; + const { classes, programStage, eventId, relationships, ready, readOnly, relationshipsLabel } = this.props; const relationshipTypes = programStage.relationshipTypes || []; const hasRelationshipTypes = relationshipTypes.length > 0; @@ -92,7 +92,7 @@ class RelationshipsSectionPlain extends React.Component { > {isEmpty && (

- {i18n.t("This event doesn't have any relationships")} + {tCustomTerm("This event doesn't have any {{relationshipsLabel}}", { relationshipsLabel })}
)} {React.createElement(LoadingRelationships as any, { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx index 63e144aeaa..10a03cfef0 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx @@ -2,6 +2,11 @@ import { connect } from 'react-redux'; import { RelationshipsSectionComponent } from './RelationshipsSection.component'; import { openAddRelationship } from '../../ViewEventComponent/viewEvent.actions'; import { requestDeleteEventRelationship } from '../../Relationship/ViewEventRelationships.actions'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; + +const customLabels = { + relationshipsLabel: { key: 'relationship', plural: true }, +} as const; const mapStateToProps = (state: any) => { const relationshipsSection = state.viewEventPage.relationshipsSection || {}; @@ -22,4 +27,6 @@ const mapDispatchToProps = (dispatch: any) => ({ }, }); -export const RelationshipsSection = connect(mapStateToProps, mapDispatchToProps)(RelationshipsSectionComponent); +export const RelationshipsSection = connect(mapStateToProps, mapDispatchToProps)( + withCustomLabels(customLabels)(RelationshipsSectionComponent), +); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts index 005b364885..38d99b7438 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts @@ -9,4 +9,5 @@ export type PlainProps = { ready: boolean; readOnly: boolean; orgUnitId: string; + relationshipsLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx index 8efcfc5148..12512cf972 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx @@ -442,6 +442,7 @@ type Props = { programName: string; orgUnitFieldValue?: OrgUnit | null; notesLabel: string; + relationshipsLabel: string; }; type DataEntrySection = { @@ -449,7 +450,7 @@ type DataEntrySection = { name: string, }; -const buildDataEntrySectionDefinitions = (notesLabel: string) => ({ +const buildDataEntrySectionDefinitions = (notesLabel: string, relationshipsLabel: string) => ({ [dataEntrySectionNames.BASICINFO]: { placement: placements.TOP, name: i18n.t('Basic info'), @@ -464,7 +465,7 @@ const buildDataEntrySectionDefinitions = (notesLabel: string) => ({ }, [dataEntrySectionNames.RELATIONSHIPS]: { placement: placements.BOTTOM, - name: i18n.t('Relationships'), + name: relationshipsLabel, }, [dataEntrySectionNames.ASSIGNEE]: { placement: placements.BOTTOM, @@ -485,7 +486,7 @@ class DataEntryPlain extends Component> { theme: props.theme, fieldLabelMediaBasedClass: props.classes.fieldLabelMediaBased, }; - this.dataEntrySections = buildDataEntrySectionDefinitions(props.notesLabel); + this.dataEntrySections = buildDataEntrySectionDefinitions(props.notesLabel, props.relationshipsLabel); } componentDidMount() { diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx index b2e71221e7..17924bf226 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx @@ -19,6 +19,7 @@ import { withCustomLabels } from '../../../HOC/withCustomLabels'; const customLabels = { notesLabel: { key: 'note', plural: true }, + relationshipsLabel: { key: 'relationship', plural: true }, } as const; const WrappedDataEntryComponent = withCustomLabels(customLabels)(DataEntryComponent); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx index 3791e98e55..01c555cd6e 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx @@ -11,6 +11,8 @@ import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollec import type { PlainProps, LinkButtonProps } from './RelatedStagesActions.types'; import { LinkToExisting } from '../LinkToExisting'; import { EnterDataInOrgUnit } from '../EnterDataInOrgUnit/EnterData.component'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const styles: Readonly = { wrapper: { @@ -207,6 +209,7 @@ const RelatedStagesActionsPlain = ({ isLinking, }: PlainProps & WithStyles) => { const { programStage } = useProgramStageInfo(constraint?.programStage?.id); + const relationshipsLabel = useTermLabel('relationship', { plural: true }); const selectedAction = useMemo(() => relatedStagesDataValues.linkMode, [relatedStagesDataValues.linkMode]); @@ -255,7 +258,10 @@ const RelatedStagesActionsPlain = ({ )} {type === relatedStageStatus.AMBIGUOUS_RELATIONSHIPS && ( -
{i18n.t('Ambiguous relationships, contact system administrator')}
+
{tCustomTerm( + 'Ambiguous {{relationshipsLabel}}, contact system administrator', + { relationshipsLabel }, + )}
)}
diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 34b9048387..10150c5892 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -1,11 +1,12 @@ import React, { useMemo } from 'react'; -import i18n from '@dhis2/d2-i18n'; import type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; import { RelationshipsWidget } from '../common/RelationshipsWidget'; import { RelationshipSearchEntities, useRelationships } from '../common/useRelationships'; import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship'; import { useTrackedEntityTypeName } from './hooks/useTrackedEntityTypeName'; import { useRelationshipTypes } from '../common/RelationshipsWidget/useRelationshipTypes'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const WidgetTrackedEntityRelationship = ({ relationshipTypes: cachedRelationshipTypes, @@ -25,6 +26,7 @@ export const WidgetTrackedEntityRelationship = ({ accessReadOnly, hideReadOnlyBadge, }: WidgetTrackedEntityRelationshipProps) => { + const relationshipsLabel = useTermLabel('relationship', { plural: true }); const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: trackedEntityTypeName, isLoading: isLoadingTEType } = useTrackedEntityTypeName(trackedEntityTypeId); const { @@ -44,7 +46,10 @@ export const WidgetTrackedEntityRelationship = ({ if (isError) { return (
- {i18n.t('Something went wrong while loading relationships. Please try again later.')} + {tCustomTerm( + 'Something went wrong while loading {{relationshipsLabel}}. Please try again later.', + { relationshipsLabel }, + )}
); } @@ -55,9 +60,9 @@ export const WidgetTrackedEntityRelationship = ({ return ( - {i18n.t("This {{enrollmentLabel}} doesn't have any relationships", { enrollmentLabel })} + {tCustomTerm( + "This {{enrollmentLabel}} doesn't have any {{relationshipsLabel}}", + { enrollmentLabel, relationshipsLabel }, + )}
)} {children} From cf5eeec544f9d41edea2bbb98cc8870dbb078ad3 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:42:06 +0000 Subject: [PATCH 039/118] fix: enrollment labels singular and plural --- i18n/en.pot | 278 +----------------- .../hooks/useOriginLabel.ts | 7 +- .../EnrollmentBreadcrumb.tsx | 3 +- .../hooks/useWorkingListLabel.ts | 7 +- .../CardList/CardListButtons.component.tsx | 3 +- .../CardList/CardListItem.component.tsx | 6 +- .../EnrollmentRegistrationEntry.component.tsx | 4 +- .../CompleteModal/CompleteModal.component.tsx | 11 +- .../DataEntryWidgetOutput.container.ts | 6 +- .../EnrollmentPageDefault.container.tsx | 6 +- .../Enrollment/MissingMessage.component.tsx | 17 +- .../Enrollment/epics/enrollmentPage.epics.ts | 17 +- .../RegistrationDataEntry.component.tsx | 4 +- .../WidgetEventEditWrapper.tsx | 3 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 3 +- .../WidgetBreakingTheGlass.component.tsx | 11 +- .../Actions/Actions.component.tsx | 3 +- .../CompleteModal/CompleteModal.component.tsx | 9 +- .../Actions/Delete/Delete.component.tsx | 11 +- .../InfoBoxes/InfoBoxes.component.tsx | 26 +- .../WidgetEnrollment.component.tsx | 2 +- .../WidgetEnrollmentNote.component.tsx | 8 +- .../RelationshipsWidget.component.tsx | 4 +- .../Setup/hooks/useFiltersOnly.ts | 3 +- .../Setup/hooks/useStaticTemplates.ts | 8 +- .../Actions/CompleteAction/CompleteAction.tsx | 32 +- .../hooks/useCompleteBulkEnrollments.ts | 8 +- .../DeleteEnrollmentsAction.tsx | 5 +- .../EnrollmentDeleteModal.tsx | 24 +- .../hooks/useDeleteEnrollments.ts | 6 +- 30 files changed, 150 insertions(+), 385 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 51500c7470..a99c03b1d0 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T09:21:02.668Z\n" -"PO-Revision-Date: 2026-08-31T09:21:02.668Z\n" +"POT-Creation-Date: 2026-08-31T09:42:07.115Z\n" +"PO-Revision-Date: 2026-08-31T09:42:07.116Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -42,21 +42,9 @@ msgstr "Bulk data entry" msgid "Program overview" msgstr "Program overview" -msgid "Active {{enrollmentsLabel}}" -msgstr "Active {{enrollmentsLabel}}" - -msgid "Completed {{enrollmentsLabel}}" -msgstr "Completed {{enrollmentsLabel}}" - -msgid "Cancelled {{enrollmentsLabel}}" -msgstr "Cancelled {{enrollmentsLabel}}" - msgid "Search" msgstr "Search" -msgid "{{enrollmentLabel}} dashboard" -msgstr "{{enrollmentLabel}} dashboard" - msgid "View event" msgstr "View event" @@ -81,9 +69,6 @@ msgstr "View {{programName}} dashboard" msgid "View dashboard" msgstr "View dashboard" -msgid "View active {{enrollmentLabel}}" -msgstr "View active {{enrollmentLabel}}" - msgid "Re-enroll in {{programName}}" msgstr "Re-enroll in {{programName}}" @@ -99,9 +84,6 @@ msgstr "Previously enrolled" msgid "Organisation unit" msgstr "Organisation unit" -msgid "Date of {{enrollmentLabel}}" -msgstr "Date of {{enrollmentLabel}}" - msgid "Last updated" msgstr "Last updated" @@ -172,9 +154,6 @@ msgstr "Please select {{categoryName}}" msgid "A date in the future is not allowed" msgstr "A date in the future is not allowed" -msgid "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." -msgstr "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." - msgid "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." msgstr "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." @@ -327,30 +306,11 @@ msgstr "An error has occurred. See log for details" msgid "{{programStageName}} completed" msgstr "{{programStageName}} completed" -msgid "" -"Would you like to complete the {{enrollmentLabel}} and all active events as " -"well?" -msgstr "" -"Would you like to complete the {{enrollmentLabel}} and all active events as " -"well?" - msgid "{{count}} event in {{programStageName}}" msgid_plural "{{count}} event in {{programStageName}}" msgstr[0] "{{count}} event in {{programStageName}}" msgstr[1] "{{count}} events in {{programStageName}}" -msgid "Yes, complete {{enrollmentLabel}} and events" -msgstr "Yes, complete {{enrollmentLabel}} and events" - -msgid "Complete {{enrollmentLabel}} only" -msgstr "Complete {{enrollmentLabel}} only" - -msgid "Would you like to complete the {{enrollmentLabel}}?" -msgstr "Would you like to complete the {{enrollmentLabel}}?" - -msgid "Complete {{enrollmentLabel}}" -msgstr "Complete {{enrollmentLabel}}" - msgid "A duplicate exists (but there were some errors, see log for details" msgstr "A duplicate exists (but there were some errors, see log for details" @@ -422,12 +382,6 @@ msgstr "Some operations are still running. Please wait." msgid "Operations running" msgstr "Operations running" -msgid "No feedback for this {{enrollmentLabel}} yet" -msgstr "No feedback for this {{enrollmentLabel}} yet" - -msgid "No indicator output for this {{enrollmentLabel}} yet" -msgstr "No indicator output for this {{enrollmentLabel}} yet" - msgid "" "This event has unsaved changes. Leaving this page without saving will lose " "these changes. Are you sure you want to discard unsaved changes?" @@ -728,28 +682,9 @@ msgstr "Make referral" msgid "No available program stages" msgstr "No available program stages" -msgid "Invalid {{enrollmentLabel}} id {{enrollmentId}}." -msgstr "Invalid {{enrollmentLabel}} id {{enrollmentId}}." - -msgid "Choose an {{enrollmentLabel}} to view the dashboard." -msgstr "Choose an {{enrollmentLabel}} to view the dashboard." - -msgid "" -"Choose a program to add new or see existing {{enrollmentsLabel}} for " -"{{teiDisplayName}}" -msgstr "" -"Choose a program to add new or see existing {{enrollmentsLabel}} for " -"{{teiDisplayName}}" - msgid "{{programName}} has categories. Choose all categories to view dashboard." msgstr "{{programName}} has categories. Choose all categories to view dashboard." -msgid "There are no active {{enrollmentsLabel}}." -msgstr "There are no active {{enrollmentsLabel}}." - -msgid "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." -msgstr "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." - msgid "" "You do not have permissions to access to this program, registering unit or " "record, contact your administrator for more information." @@ -763,15 +698,6 @@ msgstr "{{teiDisplayName}} is not enrolled in this program." msgid "Enroll {{teiDisplayName}} in this program." msgstr "Enroll {{teiDisplayName}} in this program." -msgid "" -"{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " -"{{programName}}. Choose another program that allows {{tetName}} " -"{{enrollmentLabel}}. " -msgstr "" -"{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " -"{{programName}}. Choose another program that allows {{tetName}} " -"{{enrollmentLabel}}. " - msgid "Enroll a new {{selectedTetName}} in this program." msgstr "Enroll a new {{selectedTetName}} in this program." @@ -781,9 +707,6 @@ msgstr "Create a new event in this program." msgid "View working list in this program." msgstr "View working list in this program." -msgid "Enrollment with id \"{{enrollmentId}}\" does not exist" -msgstr "Enrollment with id \"{{enrollmentId}}\" does not exist" - msgid "Tracked entity instance with id \"{{teiId}}\" does not exist" msgstr "Tracked entity instance with id \"{{teiId}}\" does not exist" @@ -897,9 +820,6 @@ msgstr "New" msgid "You can also choose a program from the top bar and create in that program" msgstr "You can also choose a program from the top bar and create in that program" -msgid "New {{enrollmentLabel}} in program{{escape}} {{programName}}" -msgstr "New {{enrollmentLabel}} in program{{escape}} {{programName}}" - msgid "Save {{trackedEntityTypeName}}" msgstr "Save {{trackedEntityTypeName}}" @@ -1044,9 +964,6 @@ msgstr "Search form is missing. See log for details" msgid "Could not retrieve metadata. Please try again later." msgstr "Could not retrieve metadata. Please try again later." -msgid "The {{enrollmentLabel}} event data could not be found" -msgstr "The {{enrollmentLabel}} event data could not be found" - msgid "Loading" msgstr "Loading" @@ -1068,9 +985,6 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "You only have view access to this {{enrollmentLabel}}" -msgstr "You only have view access to this {{enrollmentLabel}}" - msgid "You only have view access to this program" msgstr "You only have view access to this program" @@ -1300,41 +1214,18 @@ msgstr "No one is assigned to this event" msgid "Assign" msgstr "Assign" -msgid "Check for {{enrollmentsLabel}}" -msgstr "Check for {{enrollmentsLabel}}" - msgid "This program is protected" msgstr "This program is protected" -msgid "" -"You must provide a reason to check for {{enrollmentsLabel}} in this " -"protected program." -msgstr "" -"You must provide a reason to check for {{enrollmentsLabel}} in this " -"protected program." - msgid "All activity will be logged." msgstr "All activity will be logged." -msgid "Reason to check for {{enrollmentsLabel}}" -msgstr "Reason to check for {{enrollmentsLabel}}" - -msgid "" -"Describe the reason you are checking for {{enrollmentsLabel}} in this " -"protected program" -msgstr "" -"Describe the reason you are checking for {{enrollmentsLabel}} in this " -"protected program" - msgid "Unsaved changes" msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" -msgid "{{enrollmentLabel}} actions" -msgstr "{{enrollmentLabel}} actions" - msgid "We are processing your request." msgstr "We are processing your request." @@ -1350,21 +1241,6 @@ msgstr "Mark as cancelled" msgid "Mark incomplete" msgstr "Mark incomplete" -msgid "You do not have access to delete this {{enrollmentLabel}}" -msgstr "You do not have access to delete this {{enrollmentLabel}}" - -msgid "Delete {{enrollmentLabel}}" -msgstr "Delete {{enrollmentLabel}}" - -msgid "Are you sure you want to delete this {{enrollmentLabel}}?" -msgstr "Are you sure you want to delete this {{enrollmentLabel}}?" - -msgid "This will permanently remove the current {{enrollmentLabel}}." -msgstr "This will permanently remove the current {{enrollmentLabel}}." - -msgid "Yes, delete {{enrollmentLabel}}." -msgstr "Yes, delete {{enrollmentLabel}}." - msgid "Remove mark for follow-up" msgstr "Remove mark for follow-up" @@ -1407,20 +1283,6 @@ msgstr "Finish drawing before saving" msgid "Set area" msgstr "Set area" -msgid "" -"Transferring enrollment ownership from {{ownerOrgUnit}} to " -"{{newOrgUnit}}{{escape}}" -msgstr "" -"Transferring enrollment ownership from {{ownerOrgUnit}} to " -"{{newOrgUnit}}{{escape}}" - -msgid "" -"You will lose access to the enrollment when transferring ownership to " -"{{organisationUnit}}." -msgstr "" -"You will lose access to the enrollment when transferring ownership to " -"{{organisationUnit}}." - msgid "Transfer Ownership" msgstr "Transfer Ownership" @@ -1431,9 +1293,6 @@ msgstr "" "Choose the organisation unit to which {{enrollmentLabel}} ownership should " "be transferred." -msgid "{{enrollmentLabel}} date" -msgstr "{{enrollmentLabel}} date" - msgid "Incident date" msgstr "Incident date" @@ -1467,15 +1326,6 @@ msgstr "Saving to {{stageName}} for {{programName}}" msgid "Program or program stage is invalid" msgstr "Program or program stage is invalid" -msgid "Notes about this {{enrollmentLabel}}" -msgstr "Notes about this {{enrollmentLabel}}" - -msgid "Write a note about this {{enrollmentLabel}}" -msgstr "Write a note about this {{enrollmentLabel}}" - -msgid "This {{enrollmentLabel}} doesn't have any notes" -msgstr "This {{enrollmentLabel}} doesn't have any notes" - msgid "Error" msgstr "Error" @@ -1924,9 +1774,6 @@ msgstr "An error occurred while deleting the relationship." msgid "To open this relationship, please wait until saving is complete" msgstr "To open this relationship, please wait until saving is complete" -msgid "This {{enrollmentLabel}} doesn't have any relationships" -msgstr "This {{enrollmentLabel}} doesn't have any relationships" - msgid "Type" msgstr "Type" @@ -1996,133 +1843,12 @@ msgstr "Owner organisation unit" msgid "Registration Date" msgstr "Registration Date" -msgid "{{enrollmentLabel}} status" -msgstr "{{enrollmentLabel}} status" - msgid "Follow up" msgstr "Follow up" msgid "Choose a program stage to filter by {{label}}" msgstr "Choose a program stage to filter by {{label}}" -msgid "You do not have access to bulk complete {{enrollmentsLabel}}" -msgstr "You do not have access to bulk complete {{enrollmentsLabel}}" - -msgid "" -"Some {{enrollmentsLabel}} were completed successfully, but there was an " -"error while completing the rest. Please see the details below." -msgstr "" -"Some {{enrollmentsLabel}} were completed successfully, but there was an " -"error while completing the rest. Please see the details below." - -msgid "" -"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " -"Please try again." -msgstr "" -"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " -"Please try again." - -msgid "There are currently no active {{enrollmentsLabel}} in the selection." -msgstr "There are currently no active {{enrollmentsLabel}} in the selection." - -msgid "All {{enrollmentsLabel}} are already completed or cancelled." -msgstr "All {{enrollmentsLabel}} are already completed or cancelled." - -msgid "" -"This action will complete {{count}} active {{enrollmentLabel}} in your " -"selection." -msgid_plural "" -"This action will complete {{count}} active {{enrollmentLabel}} in your " -"selection." -msgstr[0] "" -"This action will complete {{count}} active {{enrollmentLabel}} in your " -"selection." -msgstr[1] "" -"This action will complete {{count}} active {{enrollmentsLabel}} in your " -"selection." - -msgid "" -"{{count}} {{enrollmentLabel}} already marked as completed will not be " -"changed." -msgid_plural "" -"{{count}} {{enrollmentLabel}} already marked as completed will not be " -"changed." -msgstr[0] "" -"{{count}} {{enrollmentLabel}} already marked as completed will not be " -"changed." -msgstr[1] "" -"{{count}} {{enrollmentsLabel}} already marked as completed will not be " -"changed." - -msgid "Mark all events within {{enrollmentsLabel}} as complete" -msgstr "Mark all events within {{enrollmentsLabel}} as complete" - -msgid "Complete {{enrollmentsLabel}}" -msgstr "Complete {{enrollmentsLabel}}" - -msgid "Error completing {{enrollmentsLabel}}" -msgstr "Error completing {{enrollmentsLabel}}" - -msgid "No active {{enrollmentsLabel}} to complete" -msgstr "No active {{enrollmentsLabel}} to complete" - -msgid "Complete {{count}} {{enrollmentLabel}}" -msgid_plural "Complete {{count}} {{enrollmentLabel}}" -msgstr[0] "Complete {{count}} {{enrollmentLabel}}" -msgstr[1] "Complete {{count}} {{enrollmentsLabel}}" - -msgid "An error occurred when completing the {{enrollmentsLabel}}" -msgstr "An error occurred when completing the {{enrollmentsLabel}}" - -msgid "An unknown error occurred when completing {{enrollmentsLabel}}" -msgstr "An unknown error occurred when completing {{enrollmentsLabel}}" - -msgid "You do not have access to delete {{enrollmentsLabel}}" -msgstr "You do not have access to delete {{enrollmentsLabel}}" - -msgid "Delete {{enrollmentsLabel}}" -msgstr "Delete {{enrollmentsLabel}}" - -msgid "Delete selected {{enrollmentsLabel}}" -msgstr "Delete selected {{enrollmentsLabel}}" - -msgid "" -"An error occurred while loading the selected {{enrollmentsLabel}}. Please " -"try again." -msgstr "" -"An error occurred while loading the selected {{enrollmentsLabel}}. Please " -"try again." - -msgid "" -"This action will permanently delete the selected {{enrollmentsLabel}}, " -"including all associated data and events." -msgstr "" -"This action will permanently delete the selected {{enrollmentsLabel}}, " -"including all associated data and events." - -msgid "Active {{enrollmentsLabel}} ({{count}})" -msgid_plural "Active {{enrollmentsLabel}} ({{count}})" -msgstr[0] "Active {{enrollmentsLabel}} ({{count}})" -msgstr[1] "Active {{enrollmentsLabel}} ({{count}})" - -msgid "Completed {{enrollmentsLabel}} ({{count}})" -msgid_plural "Completed {{enrollmentsLabel}} ({{count}})" -msgstr[0] "Completed {{enrollmentsLabel}} ({{count}})" -msgstr[1] "Completed {{enrollmentsLabel}} ({{count}})" - -msgid "Cancelled {{enrollmentsLabel}} ({{count}})" -msgid_plural "Cancelled {{enrollmentsLabel}} ({{count}})" -msgstr[0] "Cancelled {{enrollmentsLabel}} ({{count}})" -msgstr[1] "Cancelled {{enrollmentsLabel}} ({{count}})" - -msgid "Delete {{count}} {{enrollmentLabel}}" -msgid_plural "Delete {{count}} {{enrollmentLabel}}" -msgstr[0] "Delete {{count}} {{enrollmentLabel}}" -msgstr[1] "Delete {{count}} {{enrollmentsLabel}}" - -msgid "An error occurred when deleting {{enrollmentsLabel}}" -msgstr "An error occurred when deleting {{enrollmentsLabel}}" - msgid "Delete {{count}} {{ trackedEntityName }}" msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" diff --git a/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts index f3a088d899..c709201965 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { breadcrumbsKeys } from '../BulkDataEntryBreadcrumb'; import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; type Props = { programId: string; @@ -35,9 +36,9 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) const defaultFilterLabels = useMemo(() => ({ default: i18n.t('Program overview'), - active: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), - complete: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), - cancelled: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + active: tCustomTerm('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + complete: tCustomTerm('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + cancelled: tCustomTerm('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), }), [enrollmentsLabel]); const label = useMemo(() => { diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx index 2b4231e13c..633bbb4f6e 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { colors } from '@dhis2/ui'; import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { DirectionalChevron } from '../../../utils/rtl'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; import { BreadcrumbItem } from '../common/BreadcrumbItem'; @@ -103,7 +104,7 @@ const BreadcrumbsPlain = ({ { key: pageKeys.OVERVIEW, onClick: () => handleNavigation(onBackToDashboard, pageKeys.OVERVIEW), - label: i18n.t('{{enrollmentLabel}} dashboard', { enrollmentLabel }), + label: tCustomTerm('{{enrollmentLabel}} dashboard', { enrollmentLabel }), selected: page === pageKeys.OVERVIEW, condition: true, }, diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts index ab75045eb5..37c1531009 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; type Template = { id: string; @@ -38,9 +39,9 @@ export const useWorkingListLabel = ({ const defaultFilterLabels: { [key in DefaultFilterKey]: string } = useMemo(() => ({ [DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'), - [DefaultFilterKeys.ACTIVE]: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), - [DefaultFilterKeys.COMPLETE]: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), - [DefaultFilterKeys.CANCELLED]: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.ACTIVE]: tCustomTerm('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.COMPLETE]: tCustomTerm('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.CANCELLED]: tCustomTerm('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), }), [enrollmentsLabel]); const label: string = useMemo(() => { diff --git a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx index 59ec17be9c..ba8b6de8c5 100644 --- a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx @@ -9,6 +9,7 @@ import { navigateToEnrollmentOverview, } from '../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; type Props = { currentSearchScopeId?: string, @@ -117,7 +118,7 @@ const CardListButtons: FC = ({ { dataTest: 'view-active-enrollment-button', onClick: onHandleClick, - label: i18n.t('View active {{enrollmentLabel}}', { enrollmentLabel }), + label: tCustomTerm('View active {{enrollmentLabel}}', { enrollmentLabel }), hide: navigationButtonsState !== availableCardListButtonState.SHOW_VIEW_ACTIVE_ENROLLMENT_BUTTON, }, { diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx index 5177c463f0..a6a0a21b93 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx @@ -20,6 +20,7 @@ import { useTermLabel, } from '../../metaData'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; type OwnProps = { @@ -223,7 +224,10 @@ const CardListItemIndex = ({ value={orgUnitName} /> diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx index 2216ad2a97..2940f3b7a1 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx @@ -5,6 +5,7 @@ import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { compose } from 'redux'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { scopeTypes, useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { EnrollmentDataEntry } from '../Enrollment'; import type { Props, PlainProps } from './EnrollmentRegistrationEntry.types'; @@ -30,11 +31,10 @@ const translatedTextWithStylesForProgram = ( teiId?: string, ) => ( teiId ? - {i18n.t('Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}.', { + {tCustomTerm('Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}.', { enrollmentLabel, programName, orgUnitName, - interpolation: { escapeValue: false }, })} : {i18n.t('Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}.', { diff --git a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx index db8fe7a829..52a8829372 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx @@ -3,6 +3,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import type { PlainProps, PlainPropsWithEvents } from './completeModal.types'; import { useTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; export const CompleteEnrollmentAndEventsModalComponent = ({ programStageName, @@ -22,7 +23,7 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ })} -

{i18n.t( +

{tCustomTerm( 'Would you like to complete the {{enrollmentLabel}} and all active events as well?', { enrollmentLabel }, )}

@@ -71,10 +72,10 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.tsx index 60b0c1cef2..fa4fa217f4 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/InfoBoxes/InfoBoxes.component.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { cx } from '@emotion/css'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { colors, IconInfo16, IconWarning16 } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { useOrgUnitNameWithAncestors } from '../../../../metadataRetrieval/orgUnitName'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { OrgUnitScopes } from '../hooks/useTransferValidation'; import { ProgramAccessLevels } from '../hooks/useProgramAccessLevel'; @@ -48,6 +49,7 @@ const InfoBoxesPlain = ({ }: Props & WithStyles) => { const { displayName: ownerOrgUnitName } = useOrgUnitNameWithAncestors(ownerOrgUnitId); const { displayName: newOrgUnitName } = useOrgUnitNameWithAncestors(validOrgUnitId ?? null); + const enrollmentLabel = useTermLabel('enrollment'); const showWarning = [ProgramAccessLevels.PROTECTED, ProgramAccessLevels.CLOSED].includes(programAccessLevel as any) && orgUnitScopes.destination === OrgUnitScopes.SEARCH; @@ -57,20 +59,26 @@ const InfoBoxesPlain = ({ {newOrgUnitName && (
- {i18n.t('Transferring enrollment ownership from {{ownerOrgUnit}} to {{newOrgUnit}}{{escape}}', { - ownerOrgUnit: ownerOrgUnitName, - newOrgUnit: newOrgUnitName, - escape: '.', - })} + {tCustomTerm( + 'Transferring {{enrollmentLabel}} ownership from {{ownerOrgUnit}} to {{newOrgUnit}}{{escape}}', + { + enrollmentLabel, + ownerOrgUnit: ownerOrgUnitName, + newOrgUnit: newOrgUnitName, + escape: '.', + }, + )}
)} {showWarning && (
- {i18n.t('You will lose access to the enrollment when transferring ownership to {{organisationUnit}}.', { - organisationUnit: newOrgUnitName, - })} + {tCustomTerm( + 'You will lose access to the {{enrollmentLabel}} ' + + 'when transferring ownership to {{organisationUnit}}.', + { enrollmentLabel, organisationUnit: newOrgUnitName }, + )}
)}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index be81da7ff5..83b34ddfd1 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -56,7 +56,7 @@ const styles = { const getGeometryType = geometryType => (geometryType === 'Point' ? dataElementTypes.COORDINATE : dataElementTypes.POLYGON); const getEnrollmentDateLabel = (program, enrollmentLabel: string) => - program.displayEnrollmentDateLabel ?? i18n.t('{{enrollmentLabel}} date', { enrollmentLabel }); + program.displayEnrollmentDateLabel ?? tCustomTerm('{{enrollmentLabel}} date', { enrollmentLabel }); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); const WidgetEnrollmentPlain = ({ diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index b5ac821249..78c03b4ce3 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { useDispatch, useSelector } from 'react-redux'; import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; @@ -7,6 +6,7 @@ import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { useLocationQuery } from '../../utils/routing'; import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); @@ -27,9 +27,9 @@ export const WidgetEnrollmentNote = () => { return (
- {i18n.t("This {{enrollmentLabel}} doesn't have any relationships", { enrollmentLabel })} + {tCustomTerm("This {{enrollmentLabel}} doesn't have any relationships", { enrollmentLabel })}
)} {children} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts index d342f87501..c80f59e303 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { featureAvailable, FEATURES } from 'capture-core-utils'; import i18n from '@dhis2/d2-i18n'; import { dataElementTypes, type TrackerProgram, useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import { MAIN_FILTERS } from '../../constants'; export const useFiltersOnly = ( @@ -16,7 +17,7 @@ export const useFiltersOnly = ( { id: MAIN_FILTERS.PROGRAM_STATUS, type: dataElementTypes.TEXT, - header: i18n.t('{{enrollmentLabel}} status', { enrollmentLabel }), + header: tCustomTerm('{{enrollmentLabel}} status', { enrollmentLabel }), options: [ { text: i18n.t('Active'), value: 'ACTIVE' }, { text: i18n.t('Completed'), value: 'COMPLETED' }, diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts index bd86f2269b..10514ecd40 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useStaticTemplates.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import i18n from '@dhis2/d2-i18n'; import type { WorkingListTemplate } from '../../../WorkingListsBase'; import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => { const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); @@ -20,7 +20,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'active', - name: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + name: tCustomTerm('Active {{enrollmentsLabel}}', { enrollmentsLabel }), order: 1, access: { update: false, @@ -34,7 +34,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'complete', - name: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + name: tCustomTerm('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), order: 2, access: { update: false, @@ -48,7 +48,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'cancelled', - name: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + name: tCustomTerm('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), order: 3, access: { update: false, diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx index ccb0b00f3f..11bbd78e4f 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx @@ -17,6 +17,7 @@ import { useCompleteBulkEnrollments } from './hooks/useCompleteBulkEnrollments'; import { Widget } from '../../../../../Widget'; import type { PlainProps } from './CompleteAction.types'; import { useTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const styles: Readonly = { container: { @@ -43,7 +44,7 @@ const getTooltipContent = ( enrollmentsLabel: string, ) => { if (!programDataWriteAccess) { - return i18n.t('You do not have access to bulk complete {{enrollmentsLabel}}', { enrollmentsLabel }); + return tCustomTerm('You do not have access to bulk complete {{enrollmentsLabel}}', { enrollmentsLabel }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -103,8 +104,8 @@ const CompleteActionPlain = ({ {hasPartiallyUploadedEnrollments ? // eslint-disable-next-line max-len - i18n.t('Some {{enrollmentsLabel}} were completed successfully, but there was an error while completing the rest. Please see the details below.', { enrollmentsLabel }) : - i18n.t( + tCustomTerm('Some {{enrollmentsLabel}} were completed successfully, but there was an error while completing the rest. Please see the details below.', { enrollmentsLabel }) : + tCustomTerm( // eslint-disable-next-line max-len 'There was an error while completing the {{enrollmentsLabel}}. Please see the details below.', { enrollmentsLabel }, @@ -140,7 +141,7 @@ const CompleteActionPlain = ({ if (errorFetchingTrackedEntities) { return (
- {i18n.t( + {tCustomTerm( 'An unexpected error occurred while fetching the {{enrollmentsLabel}}. Please try again.', { enrollmentsLabel }, )} @@ -152,16 +153,19 @@ const CompleteActionPlain = ({ if (enrollmentCounts.active === 0) { return (
- {i18n.t('There are currently no active {{enrollmentsLabel}} in the selection.', { enrollmentsLabel })} + {tCustomTerm( + 'There are currently no active {{enrollmentsLabel}} in the selection.', + { enrollmentsLabel }, + )} {' '} - {i18n.t('All {{enrollmentsLabel}} are already completed or cancelled.', { enrollmentsLabel })} + {tCustomTerm('All {{enrollmentsLabel}} are already completed or cancelled.', { enrollmentsLabel })}
); } return (
- {i18n.t('This action will complete {{count}} active {{enrollmentLabel}} in your selection.', + {tCustomTerm('This action will complete {{count}} active {{enrollmentLabel}} in your selection.', { count: enrollmentCounts.active, enrollmentLabel, @@ -175,7 +179,7 @@ const CompleteActionPlain = ({ {' '} {enrollmentCounts.completed > 0 && - i18n.t('{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', { + tCustomTerm('{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', { count: enrollmentCounts.completed, enrollmentLabel, defaultValue: '{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', @@ -186,7 +190,7 @@ const CompleteActionPlain = ({ } setCompleteEvents(prevState => !prevState)} /> @@ -206,7 +210,7 @@ const CompleteActionPlain = ({ disabled={disabled} onClick={() => setModalIsOpen(true)} > - {i18n.t('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} + {tCustomTerm('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -216,8 +220,8 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-enrollments-dialog'} > - {validationError ? i18n.t('Error completing {{enrollmentsLabel}}', { enrollmentsLabel }) - : i18n.t('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} + {validationError ? tCustomTerm('Error completing {{enrollmentsLabel}}', { enrollmentsLabel }) + : tCustomTerm('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -235,7 +239,7 @@ const CompleteActionPlain = ({ {!validationError && ( diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx index fd6953c0ed..b5850f1bb4 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx @@ -6,6 +6,7 @@ import { useDeleteEnrollments } from '../hooks/useDeleteEnrollments'; import { CustomCheckbox } from './CustomCheckbox'; import type { PlainProps } from './EnrollmentDeleteModal.types'; import { useTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; const styles: Readonly = { modalContent: { @@ -52,12 +53,12 @@ const EnrollmentDeleteModalPlain = ({ small > - {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })} + {tCustomTerm('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })}
- {i18n.t( + {tCustomTerm( 'An error occurred while loading the selected {{enrollmentsLabel}}. Please try again.', { enrollmentsLabel }, )} @@ -84,7 +85,7 @@ const EnrollmentDeleteModalPlain = ({ onClose={() => setIsDeleteDialogOpen(false)} > - {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })} + {tCustomTerm('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -113,24 +114,27 @@ const EnrollmentDeleteModalPlain = ({ dataTest={'bulk-delete-enrollments-dialog'} > - {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })} + {tCustomTerm('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })}
{/* eslint-disable-next-line max-len */} - {i18n.t('This action will permanently delete the selected {{enrollmentsLabel}}, including all associated data and events.', { enrollmentsLabel })} + {tCustomTerm('This action will permanently delete the selected {{enrollmentsLabel}}, including all associated data and events.', { enrollmentsLabel })}
- {i18n.t('Please select which {{enrollmentLabel}} statuses you want to delete:', { enrollmentLabel })} + {tCustomTerm( + 'Please select which {{enrollmentLabel}} statuses you want to delete:', + { enrollmentLabel }, + )}
- {i18n.t('Delete {{count}} {{enrollmentLabel}}', { + {tCustomTerm('Delete {{count}} {{enrollmentLabel}}', { count: numberOfEnrollmentsToDelete, enrollmentLabel, defaultValue: 'Delete {{count}} {{enrollmentLabel}}', diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts index fadeb06aba..a5071b28aa 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts @@ -1,12 +1,12 @@ import { useCallback, useMemo, useState } from 'react'; import log from 'loglevel'; -import i18n from '@dhis2/d2-i18n'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator } from 'capture-core-utils'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; import { ReactQueryAppNamespace, useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; import { useTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; type Props = { selectedRows: Record; @@ -83,7 +83,9 @@ export const useDeleteEnrollments = ({ { onError: (error) => { log.error(errorCreator('An error occurred when deleting enrollments')({ error })); - showAlert({ message: i18n.t('An error occurred when deleting {{enrollmentsLabel}}', { enrollmentsLabel }) }); + showAlert({ + message: tCustomTerm('An error occurred when deleting {{enrollmentsLabel}}', { enrollmentsLabel }), + }); }, onSuccess: () => { queryClient.removeQueries([ReactQueryAppNamespace, ...QueryKey]); From 0ee7129bc5e02019d9cb98e73f28064d7602d889 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:56:28 +0000 Subject: [PATCH 040/118] feat: program stage labels singualr and plural --- i18n/en.pot | 52 +----- .../Filters/FiltersRows.component.tsx | 74 +++++---- .../QuickActionButton/QuickActionButton.tsx | 38 +++-- ...nrollmentAddEventPageDefault.container.tsx | 6 +- .../NewEventWorkspace.component.tsx | 8 +- .../ProgramStageSelector.container.tsx | 9 +- .../TopBar/TopBar.component.tsx | 5 +- .../EnrollmentEditEvent/TopBar.container.tsx | 5 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 18 ++- .../ReadOnlyBadge/ReadOnlyBadge.types.ts | 2 + .../DataEntry/epics/dataEntryRules.epics.ts | 8 +- .../WidgetEnrollmentEventNew.container.tsx | 6 +- .../DataEntry/editEventDataEntry.actions.ts | 8 +- .../epics/editEventDataEntry.epics.ts | 8 +- .../viewEventDataEntry.actions.ts | 8 +- .../WidgetEventSchedule.container.tsx | 7 +- .../StageCreateNewButton.tsx | 7 +- .../Stages/Stages.component.tsx | 6 +- .../WidgetStagesAndEvents.component.tsx | 6 +- .../useGroupedLinkedEntities.ts | 150 +++++++++--------- .../Setup/hooks/useProgramStageFilters.ts | 21 ++- 21 files changed, 235 insertions(+), 217 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a99c03b1d0..965bf402d8 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T09:42:07.115Z\n" -"PO-Revision-Date: 2026-08-31T09:42:07.116Z\n" +"POT-Creation-Date: 2026-08-31T10:56:29.976Z\n" +"PO-Revision-Date: 2026-08-31T10:56:29.976Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -640,9 +640,6 @@ msgstr "before or equal to" msgid "More filters" msgstr "More filters" -msgid "Program stage filters" -msgstr "Program stage filters" - msgid "Rows per page" msgstr "Rows per page" @@ -679,9 +676,6 @@ msgstr "Schedule an event" msgid "Make referral" msgstr "Make referral" -msgid "No available program stages" -msgstr "No available program stages" - msgid "{{programName}} has categories. Choose all categories to view dashboard." msgstr "{{programName}} has categories. Choose all categories to view dashboard." @@ -725,12 +719,6 @@ msgstr "There was an error opening the Page" msgid "There was an error loading the page" msgstr "There was an error loading the page" -msgid "Program stage is invalid" -msgstr "Program stage is invalid" - -msgid "Program stage not found" -msgstr "Program stage not found" - msgid "Report" msgstr "Report" @@ -749,15 +737,6 @@ msgstr "You can't add any more {{ programStageName }} events" msgid "Cancel without saving" msgstr "Cancel without saving" -msgid "Choose a program stage for a new event" -msgstr "Choose a program stage for a new event" - -msgid "Program Stages could not be loaded" -msgstr "Program Stages could not be loaded" - -msgid "Program stage" -msgstr "Program stage" - msgid "The category option is not valid for the selected organisation unit." msgstr "The category option is not valid for the selected organisation unit." @@ -994,12 +973,6 @@ msgstr "You only have view access to this {{trackedEntityName}}" msgid "You only have view access to this tracked entity type" msgstr "You only have view access to this tracked entity type" -msgid "You only have view access to these program stages" -msgstr "You only have view access to these program stages" - -msgid "You only have view access to this program stage" -msgstr "You only have view access to this program stage" - msgid "This event is outside the editing period" msgstr "This event is outside the editing period" @@ -1323,18 +1296,12 @@ msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgid "Saving to {{stageName}} for {{programName}}" msgstr "Saving to {{stageName}} for {{programName}}" -msgid "Program or program stage is invalid" -msgstr "Program or program stage is invalid" - msgid "Error" msgstr "Error" msgid "Warning" msgstr "Warning" -msgid "Program stage not found in rules execution" -msgstr "Program stage not found in rules execution" - msgid "Delete event" msgstr "Delete event" @@ -1581,9 +1548,6 @@ msgstr "Please enter a date" msgid "Please select a valid event" msgstr "Please select a valid event" -msgid "This program stage can only have one event" -msgstr "This program stage can only have one event" - msgid "New {{ eventName }} event" msgstr "New {{ eventName }} event" @@ -1631,12 +1595,6 @@ msgstr "{{ overdueEvents }} overdue" msgid "{{ scheduledEvents }} scheduled" msgstr "{{ scheduledEvents }} scheduled" -msgid "No program stages found in this program" -msgstr "No program stages found in this program" - -msgid "Program stages and events" -msgstr "Program stages and events" - msgid "An error occurred while unlinking and deleting the event." msgstr "An error occurred while unlinking and deleting the event." @@ -1780,9 +1738,6 @@ msgstr "Type" msgid "Created date" msgstr "Created date" -msgid "Program stage name" -msgstr "Program stage name" - msgid "Working list could not be loaded" msgstr "Working list could not be loaded" @@ -1846,9 +1801,6 @@ msgstr "Registration Date" msgid "Follow up" msgstr "Follow up" -msgid "Choose a program stage to filter by {{label}}" -msgstr "Choose a program stage to filter by {{label}}" - msgid "Delete {{count}} {{ trackedEntityName }}" msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" diff --git a/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx b/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx index fcb22ae905..4fe373590f 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx +++ b/src/core_modules/capture-core/components/ListView/Filters/FiltersRows.component.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { colors, spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Filters } from './Filters.component'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import type { Column, FiltersOnly, @@ -67,39 +68,44 @@ export const FiltersRowsPlain = ({ shouldRenderAdditionalFiltersButtons, visibleSelectorId, classes, -}: Props & WithStyles) => ( - <> -
- !item.additionalColumn)} - filtersOnly={filtersOnly} - additionalFilters={additionalFilters} - onUpdateFilter={onUpdateFilter} - onClearFilter={onClearFilter} - onSelectRestMenuItem={onSelectRestMenuItem} - stickyFilters={stickyFilters} - /> -
- {shouldRenderAdditionalFiltersButtons && ( - <> -
-
-
{i18n.t('Program stage filters').toUpperCase()}
+}: Props & WithStyles) => { + const programStageLabel = useTermLabel('programStage'); + return ( + <> +
+ !item.additionalColumn)} + filtersOnly={filtersOnly} + additionalFilters={additionalFilters} + onUpdateFilter={onUpdateFilter} + onClearFilter={onClearFilter} + onSelectRestMenuItem={onSelectRestMenuItem} + stickyFilters={stickyFilters} + /> +
+ {shouldRenderAdditionalFiltersButtons && ( + <>
- item.additionalColumn)} - filtersOnly={additionalFilters} - onUpdateFilter={onUpdateFilter} - onClearFilter={onClearFilter} - onSelectRestMenuItem={onSelectRestMenuItem} - stickyFilters={stickyFilters} - visibleSelectorId={visibleSelectorId} - onRemoveFilter={itemId => onRemoveFilter(itemId, stickyFilters.filtersWithValueOnInit)} - /> -
- - )} - -); +
+
+ {tCustomTerm('{{programStageLabel}} filters', { programStageLabel }).toUpperCase()} +
+
+ item.additionalColumn)} + filtersOnly={additionalFilters} + onUpdateFilter={onUpdateFilter} + onClearFilter={onClearFilter} + onSelectRestMenuItem={onSelectRestMenuItem} + stickyFilters={stickyFilters} + visibleSelectorId={visibleSelectorId} + onRemoveFilter={itemId => onRemoveFilter(itemId, stickyFilters.filtersWithValueOnInit)} + /> +
+ + )} + + ); +}; export const FiltersRowsComponent = withStyles(getStyles)(FiltersRowsPlain); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx index 608b0e6e46..4879dde742 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx @@ -1,8 +1,9 @@ import React, { type ComponentType } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { Button, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; +import { useTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; import { QuickActionButtonTypes } from './QuickActionButton.types'; const styles = { @@ -15,23 +16,26 @@ const styles = { type Props = QuickActionButtonTypes & WithStyles; -const QuickActionButtonPlain = ({ icon, label, onClickAction, dataTest, disabled = false, classes }: Props) => ( - - - ); + + ); +}; export const QuickActionButton = withStyles(styles)(QuickActionButtonPlain) as ComponentType; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container.tsx index 23d7df4412..c2503b802c 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container.tsx @@ -26,7 +26,8 @@ import type { ContainerProps } from './EnrollmentAddEventPageDefault.types'; import { WidgetsForEnrollmentEventNew } from '../PageLayout/DefaultPageLayout.constants'; import { EnrollmentAddEventPageDefaultComponent } from './EnrollmentAddEventPageDefault.component'; import { convertEventAttributeOptions } from '../../../../events/convertEventAttributeOptions'; -import { TrackerProgram } from '../../../../metaData'; +import { TrackerProgram, useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; export const EnrollmentAddEventPageDefault = ({ pageLayout, @@ -36,6 +37,7 @@ export const EnrollmentAddEventPageDefault = ({ trackedEntityInactive, }: ContainerProps) => { const { programId, stageId, orgUnitId, teiId, enrollmentId } = useLocationQuery(); + const programStageLabel = useTermLabel('programStage', { programId }); const { navigate } = useNavigate(); const dispatch = useDispatch(); @@ -146,7 +148,7 @@ export const EnrollmentAddEventPageDefault = ({ error title={i18n.t('An error has occurred')} > - {i18n.t('Program stage is invalid')} + {tCustomTerm('{{programStageLabel}} is invalid', { programStageLabel })} ); } diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx index 6122ca58cb..b6264166d9 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx @@ -4,7 +4,8 @@ import i18n from '@dhis2/d2-i18n'; import { useSelector } from 'react-redux'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { tabMode } from './newEventWorkspace.constants'; -import { getProgramAndStageForProgram, getProgramEventAccess } from '../../../../metaData'; +import { getProgramAndStageForProgram, getProgramEventAccess, useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { WidgetEnrollmentEventNew } from '../../../WidgetEnrollmentEventNew'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; @@ -48,6 +49,7 @@ const NewEventWorkspacePlain = ({ const [isWarningVisible, setWarningVisible] = useState(false); const tempMode = useRef(undefined); const { stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const programStageLabel = useTermLabel('programStage', { programId }); const onHandleSwitchTab = (newMode: string) => { if (dataEntryHasChanges) { @@ -69,7 +71,9 @@ const NewEventWorkspacePlain = ({ if (!stage) { return renderWidget( -
{i18n.t('Program stage not found')}
, +
+ {tCustomTerm('{{programStageLabel}} not found', { programStageLabel })} +
, ); } diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx index 1a3442a42b..5d48b24dea 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.container.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useMemo, useCallback } from 'react'; -import i18n from '@dhis2/d2-i18n'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import { ProgramStageSelectorComponent } from './ProgramStageSelector.component'; import { Widget } from '../../../Widget'; import { useCommonEnrollmentDomainData, useRuleEffects } from '../../common/EnrollmentOverviewDomain'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import type { Props } from './ProgramStageSelector.types'; import { useProgramFromIndexedDB } from '../../../../utils/cachedDataHooks/useProgramFromIndexedDB'; import { useNavigate, useLocationQuery, buildUrlQueryString } from '../../../../utils/routing'; @@ -14,6 +15,8 @@ import { useTrackerProgram } from '../../../../hooks/useTrackerProgram'; export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId }: Props) => { const { navigate } = useNavigate(); + const programStageLabel = useTermLabel('programStage', { programId }); + const programStagesLabel = useTermLabel('programStage', { programId, plural: true }); const { tab } = useLocationQuery(); const { error: enrollmentsError, enrollment, attributeValues } = useCommonEnrollmentDomainData( teiId, @@ -103,7 +106,7 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId <> {program ? - : i18n.t('Program Stages could not be loaded')} + : tCustomTerm('{{programStagesLabel}} could not be loaded', { programStagesLabel })} ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx index e830669970..2522d96bd2 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { useTermLabel } from '../../../../metaData'; import { ScopeSelector, SingleLockedSelect, useReset } from '../../../ScopeSelector'; import { TopBarActions } from '../../../TopBarActions'; @@ -26,6 +26,7 @@ export const EnrollmentAddEventTopBar = ({ }: Props) => { const { reset } = useReset(); const enrollmentLabel = useTermLabel('enrollment', { programId }); + const programStageLabel = useTermLabel('programStage', { programId }); return ( diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx index cf7c2d9d17..c0ddc1c364 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { dataEntryKeys } from 'capture-core/constants'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { useTermLabel, type ProgramStage } from '../../../metaData'; import { pageStatuses } from './EnrollmentEditEventPage.constants'; import { @@ -48,6 +48,7 @@ export const TopBar = ({ }: Props) => { const { setOrgUnitId } = useSetOrgUnitId(); const enrollmentLabel = useTermLabel('enrollment', { programId: programId ?? undefined }); + const programStageLabel = useTermLabel('programStage', { programId: programId ?? undefined }); const { resetProgramIdAndEnrollmentContext } = useResetProgramId(); const { resetOrgUnitId } = useResetOrgUnitId(); @@ -103,7 +104,7 @@ export const TopBar = ({ }, ]} selectedValue="alwaysPreselected" - title={i18n.t('Program stage')} + title={capitalizeFirstLetter(programStageLabel)} isUserInteractionInProgress={isUserInteractionInProgress} /> {programStage && ( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index f168800d7f..71ee21ba10 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -22,9 +22,13 @@ const getTrackedEntityMessage = (trackedEntityName: string | undefined): string ? i18n.t('You only have view access to this {{trackedEntityName}}', { trackedEntityName, escapeValue: false }) : i18n.t('You only have view access to this tracked entity type')); -const getProgramStageMessage = (multipleStages: boolean): string => (multipleStages - ? i18n.t('You only have view access to these program stages') - : i18n.t('You only have view access to this program stage')); +const getProgramStageMessage = ( + multipleStages: boolean, + programStageLabel: string, + programStagesLabel: string, +): string => (multipleStages + ? tCustomTerm('You only have view access to these {{programStagesLabel}}', { programStagesLabel }) + : tCustomTerm('You only have view access to this {{programStageLabel}}', { programStageLabel })); const getExpiredMessage = (): string => i18n.t('This event is outside the editing period'); @@ -44,12 +48,14 @@ const getReadOnlyMessage = ({ withinCompleteEventsExpiry, trackedEntityInactive, enrollmentLabel, + programStageLabel, + programStagesLabel, }: ReadOnlyMessageInput): string => { if (trackedEntityInactive) return getDeactivatedMessage(trackedEntityName); if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(enrollmentLabel); if (!access.program) return getProgramMessage(); if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); - if (!access.programStage) return getProgramStageMessage(multipleStages); + if (!access.programStage) return getProgramStageMessage(multipleStages, programStageLabel, programStagesLabel); if (!eventWithinValidPeriod) return getExpiredMessage(); if (!canEditCompletedEvent) return getCompletedEventMessage(); if (!withinCompleteEventsExpiry) return getExpiredMessage(); @@ -70,6 +76,8 @@ const ReadOnlyBadgePlain = ({ classes, }: Props & WithStyles) => { const enrollmentLabel = useTermLabel('enrollment'); + const programStageLabel = useTermLabel('programStage'); + const programStagesLabel = useTermLabel('programStage', { plural: true }); const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, @@ -84,6 +92,8 @@ const ReadOnlyBadgePlain = ({ withinCompleteEventsExpiry, trackedEntityInactive, enrollmentLabel, + programStageLabel, + programStagesLabel, }); if (!message) return null; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts index d1f94a4f4b..1f783f04f9 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts @@ -26,4 +26,6 @@ export type ReadOnlyMessageInput = { withinCompleteEventsExpiry: boolean; trackedEntityInactive: boolean; enrollmentLabel: string; + programStageLabel: string; + programStagesLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts index b70fee17be..5a5fa84748 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts @@ -2,9 +2,9 @@ import { ofType } from 'redux-observable'; import { map, concatMap } from 'rxjs/operators'; import { from } from 'rxjs'; import { batchActions } from 'redux-batched-actions'; -import i18n from '@dhis2/d2-i18n'; import { ReduxStore, ApiUtils } from 'capture-core-utils/types/global'; -import { getTrackerProgramThrowIfNotFound } from '../../../../metaData/helpers'; +import { getTrackerProgramThrowIfNotFound, getTermLabel } from '../../../../metaData/helpers'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { rulesExecutedPostUpdateField } from '../../../DataEntry/actions/dataEntry.actions'; import { newEventWidgetDataEntryActionTypes, @@ -49,7 +49,9 @@ const runRulesForNewEvent = async ({ const program = getTrackerProgramThrowIfNotFound(programId); const stage = program.getStage(stageId); if (!stage) { - throw Error(i18n.t('Program stage not found')); + throw Error(tCustomTerm('{{programStageLabel}} not found', { + programStageLabel: getTermLabel(programId, 'programStage'), + })); } const foundation = stage.stageForm; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx index 5c2a625856..d43fc1b69f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx @@ -1,9 +1,10 @@ import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; -import { getProgramAndStageForProgram, TrackerProgram } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, useTermLabel } from '../../metaData'; import { OrgUnitFetcher } from './OrgUnitFetcher/OrgUnitFetcher.container'; import type { WidgetProps } from './WidgetEnrollmentEventNew.types'; import { useMetadataForProgramStage } from '../DataEntries/common/ProgramStage/useMetadataForProgramStage'; +import { tCustomTerm } from '../../utils/tCustomTerm'; export const WidgetEnrollmentEventNew = ({ programId, @@ -12,6 +13,7 @@ export const WidgetEnrollmentEventNew = ({ ...passOnProps }: WidgetProps) => { const { program } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const programStageLabel = useTermLabel('programStage', { programId }); const { stage, formFoundation, @@ -30,7 +32,7 @@ export const WidgetEnrollmentEventNew = ({ if (!program || !stage || !(program instanceof TrackerProgram) || isError || !formFoundation) { return (
- {i18n.t('Program or program stage is invalid')} + {tCustomTerm('Program or {{programStageLabel}} is invalid', { programStageLabel })}
); } diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts index 10d9a263fb..c081432dd8 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts @@ -1,14 +1,14 @@ -import i18n from '@dhis2/d2-i18n'; import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import type { ReduxAction } from 'capture-core-utils/types'; import { actionCreator, actionPayloadAppender } from '../../../actions/actions.utils'; +import { getTermLabel, RenderFoundation, Program } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { getDataEntryKey } from '../../DataEntry/common/getDataEntryKey'; import { getApplicableRuleEffectsForEventProgram, getApplicableRuleEffectsForTrackerProgram, updateRulesEffects, } from '../../../rules'; -import { RenderFoundation, Program } from '../../../metaData'; import { getEventDateValidatorContainers, getOrgUnitValidatorContainers, @@ -165,7 +165,9 @@ export const openEventForEditInDataEntry = ({ if (program instanceof TrackerProgram) { const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { - throw Error(i18n.t('Program stage not found in rules execution')); + throw Error(tCustomTerm('{{programStageLabel}} not found in rules execution', { + programStageLabel: getTermLabel(program?.id, 'programStage'), + })); } // TODO: Add attributeValues & enrollmentData effects = getApplicableRuleEffectsForTrackerProgram({ diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts index eee466d3b6..255edb572b 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts @@ -1,4 +1,3 @@ -import i18n from '@dhis2/d2-i18n'; import { from } from 'rxjs'; import { ofType } from 'redux-observable'; import { map, concatMap } from 'rxjs/operators'; @@ -9,7 +8,8 @@ import { batchActionTypes as editEventDataEntryBatchActionTypes, actionTypes as editEventDataEntryActionTypes, } from '../editEventDataEntry.actions'; -import { getProgramThrowIfNotFound, dataElementTypes } from '../../../../metaData'; +import { getProgramThrowIfNotFound, dataElementTypes, getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { convertValue } from '../../../../converters/serverToClient'; import { getCurrentClientValues, @@ -56,7 +56,9 @@ const runRulesForEditSingleEvent = async ({ : getStageFromEvent(event)?.stage; if (!stage) { - throw Error(i18n.t('Program stage not found in rules execution')); + throw Error(tCustomTerm('{{programStageLabel}} not found in rules execution', { + programStageLabel: getTermLabel(programId, 'programStage'), + })); } const foundation = stage.stageForm; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts index 026a42ccdf..eb427ebb6e 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts @@ -1,7 +1,8 @@ -import i18n from '@dhis2/d2-i18n'; import { type OrgUnit, effectActions } from '@dhis2/rules-engine-javascript'; import { actionCreator } from '../../../actions/actions.utils'; import type { RenderFoundation, Program } from '../../../metaData'; +import { getTermLabel, dataElementTypes } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { getConvertGeometryIn, convertGeometryOut, convertStatusOut } from '../../DataEntries'; import { getDataEntryKey } from '../../DataEntry/common/getDataEntryKey'; import { loadEditDataEntryAsync } from '../../DataEntry/templates/dataEntryLoadEdit.template'; @@ -11,7 +12,6 @@ import { updateRulesEffects, filterApplicableRuleEffects, } from '../../../rules'; -import { dataElementTypes } from '../../../metaData'; import { convertClientToForm } from '../../../converters'; import type { ClientEventContainer } from '../../../events/eventRequests'; import { TrackerProgram, EventProgram } from '../../../metaData/Program'; @@ -145,7 +145,9 @@ export const loadViewEventDataEntry = if (program instanceof TrackerProgram) { const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { - throw Error(i18n.t('Program stage not found in rules execution')); + throw Error(tCustomTerm('{{programStageLabel}} not found in rules execution', { + programStageLabel: getTermLabel(program?.id, 'programStage'), + })); } effects = getApplicableRuleEffectsForTrackerProgram({ diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx index 7803a97d33..d91d79692e 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { useDispatch } from 'react-redux'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import moment from 'moment'; import { pipe } from 'capture-core-utils'; -import { getProgramAndStageForProgram, TrackerProgram, dataElementTypes } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, dataElementTypes, useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import { getCachedOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import { CurrentUser } from '../../utils/userInfo/CurrentUser'; @@ -38,6 +38,7 @@ export const WidgetEventSchedule = ({ ...passOnProps }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const programStageLabel = useTermLabel('programStage', { programId }); const dispatch = useDispatch(); const { programStageScheduleConfig }: {programStageScheduleConfig?: any} = useScheduleConfigFromProgramStage(stageId); const { programConfig }: {programConfig?: any} = useScheduleConfigFromProgram(programId); @@ -181,7 +182,7 @@ export const WidgetEventSchedule = ({ if (!program || !stage || !(program instanceof TrackerProgram) || !programStageScheduleConfig) { return (
- {i18n.t('Program or program stage is invalid')} + {tCustomTerm('Program or {{programStageLabel}} is invalid', { programStageLabel })}
); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index f1bd8bea3c..198a2e104f 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -2,6 +2,8 @@ import React, { useMemo } from 'react'; import { Button, IconAdd16 } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../../../../Tooltips/ConditionalTooltip'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; type Props = { onCreateNew: () => void; @@ -18,6 +20,7 @@ export const StageCreateNewButton = ({ preventAddingEventActionInEffect, eventName, }: Props) => { + const programStageLabel = useTermLabel('programStage'); const { isDisabled, tooltipContent } = useMemo(() => { if (preventAddingEventActionInEffect) { return { @@ -31,14 +34,14 @@ export const StageCreateNewButton = ({ if (!repeatable && eventCount > 0) { return { isDisabled: true, - tooltipContent: i18n.t('This program stage can only have one event'), + tooltipContent: tCustomTerm('This {{programStageLabel}} can only have one event', { programStageLabel }), }; } return { isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, programStageLabel]); return ( { const { stageReadAccessById } = useEnrollmentAccessContext(); + const programStagesLabel = useTermLabel('programStage', { programId: passOnProps.programId, plural: true }); const readableStages = useMemo( () => stages.filter(stage => stageReadAccessById[stage.id] ?? stage.dataAccess.read), [stages, stageReadAccessById], @@ -52,7 +54,7 @@ export const StagesPlain = ({ if (!readableStages.length) { return (

- {i18n.t('No program stages found in this program')} + {tCustomTerm('No {{programStagesLabel}} found in this program', { programStagesLabel })}

); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 13a96518fc..db9f69b6c1 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -1,11 +1,12 @@ import React, { useState, useCallback } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import type { Props } from './stagesAndEvents.types'; const styles = { @@ -29,6 +30,7 @@ const WidgetStagesAndEventsPlain = ({ ...passOnProps }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); + const programStagesLabel = useTermLabel('programStage', { programId, plural: true }); const { anyStageWriteAccess, anyStageReadAccess, @@ -44,7 +46,7 @@ const WidgetStagesAndEventsPlain = ({ - {i18n.t('Program stages and events')} + {tCustomTerm('{{programStagesLabel}} and events', { programStagesLabel })} {showWidgetBadge && (
({ [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ id: 'trackedEntityTypeName', displayName: i18n.t('Type'), @@ -23,7 +24,7 @@ const getFallbackFieldsByRelationshipEntity = { }], [RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE]: () => [{ id: 'programStageName', - displayName: i18n.t('Program stage name'), + displayName: tCustomTerm('{{programStageLabel}} name', { programStageLabel }), convertValue: (programStageName: any) => programStageName, }, { @@ -33,9 +34,9 @@ const getFallbackFieldsByRelationshipEntity = { convertServerToClient(createdDate, dataElementTypes.DATE), dataElementTypes.DATE, ), }], -}; +}); -const getColumns = ({ relationshipEntity, trackerDataView }: any) => { +const getColumns = ({ relationshipEntity, trackerDataView }: any, programStageLabel: string) => { let fields; if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { fields = trackerDataView.attributes; @@ -44,7 +45,7 @@ const getColumns = ({ relationshipEntity, trackerDataView }: any) => { } if (!fields?.length) { - fields = getFallbackFieldsByRelationshipEntity[relationshipEntity](); + fields = getFallbackFieldsByRelationshipEntity(programStageLabel)[relationshipEntity](); } return fields; @@ -169,73 +170,76 @@ export const useGroupedLinkedEntities = ( relationshipTypes: RelationshipTypes | null | undefined, relationships?: Array, readOnly?: boolean, -): GroupedLinkedEntities => useMemo(() => { - if (!relationships?.length || !relationshipTypes?.length) { - return []; - } - - return relationships - .sort((a, b) => moment(b.createdAt) - .diff(moment(a.createdAt))) - .reduce((accGroupedLinkedEntities, relationship) => { - const { - relationship: relationshipId, - relationshipType: relationshipTypeId, - from: fromEntity, - to: toEntity, - pendingApiResponse, - createdAt: relationshipCreatedAt, - } = relationship; - - const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); - if (!relationshipType) { - log.error( - errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), - ); - return accGroupedLinkedEntities; - } - - const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); - if (!apiLinkedEntity) { - return accGroupedLinkedEntities; - } +): GroupedLinkedEntities => { + const programStageLabel = useTermLabel('programStage'); + return useMemo(() => { + if (!relationships?.length || !relationshipTypes?.length) { + return []; + } - if (!relationshipType.bidirectional && apiLinkedEntity === fromEntity) { - return accGroupedLinkedEntities; - } + return relationships + .sort((a, b) => moment(b.createdAt) + .diff(moment(a.createdAt))) + .reduce((accGroupedLinkedEntities, relationship) => { + const { + relationship: relationshipId, + relationshipType: relationshipTypeId, + from: fromEntity, + to: toEntity, + pendingApiResponse, + createdAt: relationshipCreatedAt, + } = relationship; + + const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); + if (!relationshipType) { + log.error( + errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), + ); + return accGroupedLinkedEntities; + } + + const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); + if (!apiLinkedEntity) { + return accGroupedLinkedEntities; + } + + if (!relationshipType.bidirectional && apiLinkedEntity === fromEntity) { + return accGroupedLinkedEntities; + } + + const linkedEntityData = getLinkedEntityData( + apiLinkedEntity, + { relationshipCreatedAt, relationshipId }, + pendingApiResponse); + if (!linkedEntityData) { + return accGroupedLinkedEntities; + } + + const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; + const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); + if (group) { + group.linkedEntities = [ + ...group.linkedEntities, + linkedEntityData, + ]; + } else { + const { constraint, name } = apiLinkedEntity === fromEntity ? + { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : + { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; + + const columns = getColumns(constraint, programStageLabel); + const context = getContext(constraint, relationshipType.access, readOnly); + + accGroupedLinkedEntities.push({ + id: groupId, + name: name || relationshipType.displayName, + linkedEntities: [linkedEntityData], + columns, + context, + }); + } - const linkedEntityData = getLinkedEntityData( - apiLinkedEntity, - { relationshipCreatedAt, relationshipId }, - pendingApiResponse); - if (!linkedEntityData) { return accGroupedLinkedEntities; - } - - const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; - const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); - if (group) { - group.linkedEntities = [ - ...group.linkedEntities, - linkedEntityData, - ]; - } else { - const { constraint, name } = apiLinkedEntity === fromEntity ? - { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : - { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; - - const columns = getColumns(constraint); - const context = getContext(constraint, relationshipType.access, readOnly); - - accGroupedLinkedEntities.push({ - id: groupId, - name: name || relationshipType.displayName, - linkedEntities: [linkedEntityData], - columns, - context, - }); - } - - return accGroupedLinkedEntities; - }, [] as any); -}, [relationships, relationshipTypes, sourceId, readOnly]); + }, [] as any); + }, [relationships, relationshipTypes, sourceId, readOnly, programStageLabel]); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useProgramStageFilters.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useProgramStageFilters.ts index 9c3a40b2a9..42ccf49db5 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useProgramStageFilters.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useProgramStageFilters.ts @@ -1,7 +1,14 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { statusTypes, translatedStatusTypes } from 'capture-core/events/statusTypes'; -import { type TrackerProgram, type ProgramStage, dataElementTypes, getProgramEventAccess } from '../../../../../metaData'; +import { + type TrackerProgram, + type ProgramStage, + dataElementTypes, + getProgramEventAccess, + useTermLabel, +} from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import { ADDITIONAL_FILTERS, ADDITIONAL_FILTERS_LABELS } from '../../helpers'; const useProgramStageData = (programStageId, stages) => @@ -44,6 +51,7 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: program.stages, ); const options: Array<{ text: string, value: string }> = useProgramStageDropdowOptions(program.stages, program.id); + const programStageLabel = useTermLabel('programStage', { programId: program.id }); return useMemo(() => { const translatedStatus = translatedStatusTypes(); @@ -61,7 +69,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: type: dataElementTypes.DATE, header: occurredAtLabel, disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + tooltipContent: tCustomTerm('Choose a {{programStageLabel}} to filter by {{label}}', { + programStageLabel, label: occurredAtLabel, interpolation: { escapeValue: false }, }), @@ -91,7 +100,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: { text: translatedStatus.SKIPPED, value: statusTypes.SKIPPED }, ], disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + tooltipContent: tCustomTerm('Choose a {{programStageLabel}} to filter by {{label}}', { + programStageLabel, label: ADDITIONAL_FILTERS_LABELS.status, interpolation: { escapeValue: false }, }), @@ -106,7 +116,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: type: dataElementTypes.DATE, header: scheduledAtLabel, disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + tooltipContent: tCustomTerm('Choose a {{programStageLabel}} to filter by {{label}}', { + programStageLabel, label: scheduledAtLabel, interpolation: { escapeValue: false }, }), @@ -144,5 +155,5 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: ] : []), ]; - }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options, enableUserAssignment]); + }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options, enableUserAssignment, programStageLabel]); }; From f6b53b02b9b0c1c216d1bc5961ea15e699e65ad8 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:01:16 +0000 Subject: [PATCH 041/118] feat: attributes labels plural --- i18n/en.pot | 29 ++----------------- .../capture-core/HOC/withCustomLabels.tsx | 8 +++-- .../TeiSearch/TeiSearch.component.tsx | 3 +- .../TeiSearch/TeiSearch.container.ts | 7 ++++- .../TeiSearch/TeiSearch.types.ts | 6 +++- .../TeiSearchForm/TeiSearchForm.component.tsx | 4 ++- .../TeiSearchForm/TeiSearchForm.container.ts | 9 +++++- .../SearchBox/SearchBox.component.tsx | 10 +++++-- .../SearchForm/SearchForm.component.tsx | 6 +++- .../SearchResults/SearchResults.component.tsx | 9 ++++-- .../SearchStatus/SearchStatus.component.tsx | 8 ++++- .../TeiSearch/TeiSearch.component.tsx | 3 +- .../TeiSearch/TeiSearch.container.ts | 9 +++++- .../components/TeiSearch/TeiSearch.types.ts | 6 +++- .../TeiSearchForm/TeiSearchForm.component.tsx | 3 +- .../TeiSearchForm/TeiSearchForm.container.ts | 9 +++++- .../TeiSearchForm/TeiSearchForm.types.ts | 1 + .../WidgetProfile/WidgetProfile.component.tsx | 9 ++++-- ...portedAttributesNotification.component.tsx | 5 +++- 19 files changed, 92 insertions(+), 52 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 05bffb2275..1ca6283c80 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T09:36:32.334Z\n" -"PO-Revision-Date: 2026-08-31T09:36:32.335Z\n" +"POT-Creation-Date: 2026-08-31T11:01:18.198Z\n" +"PO-Revision-Date: 2026-08-31T11:01:18.198Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1006,9 +1006,6 @@ msgstr "Selected program" msgid "Search {{uniqueAttrName}}" msgstr "Search {{uniqueAttrName}}" -msgid "Search by attributes" -msgstr "Search by attributes" - msgid "Search {{attributeName}}" msgstr "Search {{attributeName}}" @@ -1147,9 +1144,6 @@ msgstr "" msgid "Choose a type to start searching" msgstr "Choose a type to start searching" -msgid "{{trackedEntityName}} has no searchable attributes" -msgstr "{{trackedEntityName}} has no searchable attributes" - msgid "" "Try selecting a different tracked entity type, or try searching in a " "program by choosing one from the top bar." @@ -1172,9 +1166,6 @@ msgstr "all programs" msgid "Not finding the results you were looking for? Try searching in all programs." msgstr "Not finding the results you were looking for? Try searching in all programs." -msgid "No searchable attributes for {{trackedEntityName}}" -msgstr "No searchable attributes for {{trackedEntityName}}" - msgid "Search in all programs" msgstr "Search in all programs" @@ -1223,13 +1214,6 @@ msgstr "Too many results" msgid "This search returned too many results to show." msgstr "This search returned too many results to show." -msgid "" -"Try changing search terms or searching by more attributes to narrow down " -"the results." -msgstr "" -"Try changing search terms or searching by more attributes to narrow down " -"the results." - msgid "Cannot search in all programs" msgstr "Cannot search in all programs" @@ -1650,12 +1634,6 @@ msgstr "View profile" msgid "Profile widget could not be loaded. Please try again later" msgstr "Profile widget could not be loaded. Please try again later" -msgid "No attributes configured for {{trackedEntityTypeName}}" -msgstr "No attributes configured for {{trackedEntityTypeName}}" - -msgid "No attributes configured" -msgstr "No attributes configured" - msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" @@ -2334,9 +2312,6 @@ msgstr[1] "" "The following attribute types are not supported for searching and have been " "hidden" -msgid "Some attributes are hidden" -msgstr "Some attributes are hidden" - msgid "Set coordinate" msgstr "Set coordinate" diff --git a/src/core_modules/capture-core/HOC/withCustomLabels.tsx b/src/core_modules/capture-core/HOC/withCustomLabels.tsx index 659b0957ad..3b16aa9c49 100644 --- a/src/core_modules/capture-core/HOC/withCustomLabels.tsx +++ b/src/core_modules/capture-core/HOC/withCustomLabels.tsx @@ -14,13 +14,15 @@ type InjectedLabels = { [K in keyof S]: string }; export const withCustomLabels = (specs: S) => -

>(WrappedComponent: React.ComponentType

>) => - (props: P) => { +

>( + WrappedComponent: React.ComponentType

>, + ): React.ComponentType>> => + (props: Omit>) => { const labels = Object.fromEntries( Object.entries(specs).map(([propName, { key, plural }]) => [ propName, capitalizeFirstLetter(useTermLabel(key, { plural })), ]), ) as InjectedLabels; - return React.createElement(WrappedComponent, { ...props, ...labels }); + return React.createElement(WrappedComponent, { ...props, ...labels } as unknown as P & InjectedLabels); }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.tsx index 4828800d3a..0548675bb4 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.component.tsx @@ -7,6 +7,7 @@ import { TeiSearchResults } from './TeiSearchResults/TeiSearchResults.container' import { SearchProgramSelector } from './SearchProgramSelector/SearchProgramSelector.container'; import { Section, SectionHeaderSimple } from '../../../../Section'; import { ResultsPageSizeContext } from '../../../shared-contexts'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import type { Props } from './TeiSearch.types'; const getStyles = (theme: any) => ({ @@ -99,7 +100,7 @@ const TeiSearchPlain = (props: Props & WithStyles) => { const isUnique = sg.unique; const header = isUnique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : - i18n.t('Search by attributes'); + tCustomTerm('Search by {{attributesLabel}}', { attributesLabel: props.attributesLabel }); const collapsed = props.openSearchGroupSection !== searchGroupId; const unsupportedAttributes = sg.unsupportedAttributes; return ( diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.ts index a6e0b3b1c4..adac9e8b08 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.container.ts @@ -12,6 +12,11 @@ import { import type { OwnProps } from './TeiSearch.types'; import { getSearchGroups } from './getSearchGroups'; import { getTrackedEntityTypeThrowIfNotFound } from '../../../../../metaData'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; + +const customLabels = { + attributesLabel: { key: 'attribute', plural: true }, +} as const; const mapStateToProps = (state: any, props: OwnProps) => { const currentTeiSearch = state.teiSearch[props.id] ?? {}; @@ -49,4 +54,4 @@ const mapDispatchToProps = (dispatch: any, ownProps: OwnProps) => ({ }); export const TeiSearch: ComponentType = - connect(mapStateToProps, mapDispatchToProps)(TeiSearchComponent); + connect(mapStateToProps, mapDispatchToProps)(withCustomLabels(customLabels)(TeiSearchComponent)); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.ts index 0e91e4af96..09f56fce19 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearch.types.ts @@ -24,4 +24,8 @@ export type OwnProps = { selectedTrackedEntityTypeId: string; } -export type Props = OwnProps & DispatchersFromRedux & PropsFromRedux; +type InjectedLabels = { + attributesLabel: string; +} + +export type Props = OwnProps & DispatchersFromRedux & PropsFromRedux & InjectedLabels; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx index 9aaf7e90fe..2d4207dbea 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx @@ -16,6 +16,7 @@ import { D2Form } from '../../../../../D2Form'; import { SearchOrgUnitSelector } from '../SearchOrgUnitSelector/SearchOrgUnitSelector.container'; import { withGotoInterface } from '../../../../../FormFields/New'; import type { SearchGroup } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); @@ -55,6 +56,7 @@ type OwnProps = { searchGroup: SearchGroup; attributesWithValuesCount: number; formsValues: { [formElement: string]: any }; + attributesLabel: string; }; type Props = OwnProps & WithStyles; @@ -217,7 +219,7 @@ class SearchFormPlain extends React.Component { } const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) : - i18n.t('Search by attributes'); + tCustomTerm('Search by {{attributesLabel}}', { attributesLabel: this.props.attributesLabel }); return (

{ const formValues = state.formsValues[formId] || {}; @@ -19,4 +24,6 @@ const mapStateToProps = (state: any, props: any) => { }; }; -export const TeiSearchForm = connect(mapStateToProps, () => ({}))(TeiSearchFormComponent); +export const TeiSearchForm = connect(mapStateToProps, () => ({}))( + withCustomLabels(customLabels)(TeiSearchFormComponent), +); diff --git a/src/core_modules/capture-core/components/SearchBox/SearchBox.component.tsx b/src/core_modules/capture-core/components/SearchBox/SearchBox.component.tsx index 4f5815780c..a3385b085d 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchBox.component.tsx +++ b/src/core_modules/capture-core/components/SearchBox/SearchBox.component.tsx @@ -14,7 +14,8 @@ import { searchScopes } from './SearchBox.constants'; import { useScopeTitleText, useScopeInfo } from '../../hooks'; import { useSearchOption } from './hooks'; import { SearchStatus } from './SearchStatus'; -import { scopeTypes } from '../../metaData'; +import { scopeTypes, useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; const getStyles: Readonly = { half: { @@ -91,6 +92,7 @@ function renderFooterContent(args: { searchGroupsForSelectedScope: SearchGroups; availableSearchOption?: AvailableSearchOption; trackedEntityName: string; + attributesLabel: string; }) { if (args.isLoading) { return ; @@ -114,9 +116,9 @@ function renderFooterContent(args: { footerNodes.push( {/* eslint-disable-next-line max-len */} @@ -145,6 +147,7 @@ const Index = ({ ); const { trackedEntityName } = useScopeInfo(selectedSearchScopeId ?? null); const titleText = useScopeTitleText(selectedSearchScopeId ?? null); + const attributesLabel = useTermLabel('attribute', { plural: true }); const { searchOption: availableSearchOption, isLoading, @@ -212,6 +215,7 @@ const Index = ({ searchGroupsForSelectedScope, availableSearchOption, trackedEntityName, + attributesLabel, })} ); diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.tsx b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.tsx index 5fc334721a..034f008f4e 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.tsx +++ b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.tsx @@ -9,6 +9,8 @@ import { Section, SectionHeaderSimple } from '../../Section'; import type { Props } from './SearchForm.types'; import { searchBoxStatus } from '../../../reducers/descriptions/searchDomain.reducerDescription'; import { ResultsPageSizeContext } from '../../Pages/shared-contexts'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const styles: Readonly = (theme: any) => ({ searchDomainsContainer: { @@ -102,6 +104,7 @@ const SearchFormIndex = ({ keptFallbackSearchFormValues, }: Props & WithStyles) => { const { resultsPageSize } = useContext(ResultsPageSizeContext) as any; + const attributesLabel = useTermLabel('attribute', { plural: true }); useFormDataLifecycle( searchGroupsForSelectedScope, @@ -288,7 +291,7 @@ const SearchFormIndex = ({ minAttributesRequiredToSearch, unsupportedAttributes, }) => { - const searchByText = i18n.t('Search by attributes'); + const searchByText = tCustomTerm('Search by {{attributesLabel}}', { attributesLabel }); const isSearchSectionCollapsed = !(expandedFormId === formId); return (
@@ -375,6 +378,7 @@ const SearchFormIndex = ({ resultsPageSize, error, expandedFormId, + attributesLabel, ]); }; diff --git a/src/core_modules/capture-core/components/SearchBox/SearchResults/SearchResults.component.tsx b/src/core_modules/capture-core/components/SearchBox/SearchResults/SearchResults.component.tsx index 0ba4299708..fdbc968406 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchResults/SearchResults.component.tsx +++ b/src/core_modules/capture-core/components/SearchBox/SearchResults/SearchResults.component.tsx @@ -18,7 +18,8 @@ import { SearchResultsHeader } from '../../SearchResultsHeader'; import { ResultsPageSizeContext } from '../../Pages/shared-contexts'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { Widget } from '../../Widget'; -import { getTrackerProgramThrowIfNotFound } from '../../../metaData'; +import { getTrackerProgramThrowIfNotFound, useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const SearchPagination = withNavigation()(Pagination); @@ -64,6 +65,7 @@ const SearchResultsIndex = ({ orgUnitId, }: Props & WithStyles) => { const { resultsPageSize } = useContext(ResultsPageSizeContext) as any; + const attributesLabel = useTermLabel('attribute', { plural: true }); const [isTopResultsOpen, setTopResultsOpen] = useState(true); const [isOtherResultsOpen, setOtherResultsOpen] = useState(true); const [isFallbackLoading, setIsFallbackLoading] = useState(false); @@ -210,8 +212,9 @@ const SearchResultsIndex = ({
); diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx index 5db90c8754..0dda748283 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx @@ -8,6 +8,7 @@ import { TeiSearchResults } from './TeiSearchResults/TeiSearchResults.container' import { SearchProgramSelector } from './SearchProgramSelector/SearchProgramSelector.container'; import { Section, SectionHeaderSimple } from '../Section'; import { ResultsPageSizeContext } from '../Pages/shared-contexts'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import type { Props } from './TeiSearch.types'; const styles: Readonly = (theme: any) => ({ @@ -105,7 +106,7 @@ class TeiSearchPlain extends React.Component, const isUnique = sg.unique; const header = isUnique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : - i18n.t('Search by attributes'); + tCustomTerm('Search by {{attributesLabel}}', { attributesLabel: this.props.attributesLabel }); const collapsed = this.props.openSearchGroupSection !== searchGroupId; const unsupportedAttributes = sg.unsupportedAttributes; return ( diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.container.ts b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.container.ts index 17c3c6611c..f744233664 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.container.ts +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.container.ts @@ -10,6 +10,11 @@ import { } from './actions/teiSearch.actions'; import { makeSearchGroupsSelector } from './teiSearch.selectors'; import type { OwnProps } from './TeiSearch.types'; +import { withCustomLabels } from '../../HOC/withCustomLabels'; + +const customLabels = { + attributesLabel: { key: 'attribute', plural: true }, +} as const; const makeMapStateToProps = () => { const searchGroupsSelector = makeSearchGroupsSelector(); @@ -50,4 +55,6 @@ const mapDispatchToProps = (dispatch: any, ownProps: OwnProps) => ({ }, }); -export const TeiSearch = connect(makeMapStateToProps, mapDispatchToProps)(TeiSearchComponent); +export const TeiSearch = connect(makeMapStateToProps, mapDispatchToProps)( + withCustomLabels(customLabels)(TeiSearchComponent), +); diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts index 82aeae9670..76cffa0e48 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts @@ -23,4 +23,8 @@ export type OwnProps = { resultsPageSize: number; }; -export type Props = OwnProps & DispatchersFromRedux & PropsFromRedux; +type InjectedLabels = { + attributesLabel: string; +}; + +export type Props = OwnProps & DispatchersFromRedux & PropsFromRedux & InjectedLabels; diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx index b91014722b..b34b7bacf4 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx @@ -16,6 +16,7 @@ import { D2Form } from '../../D2Form'; import { SearchOrgUnitSelector } from '../SearchOrgUnitSelector/SearchOrgUnitSelector.container'; import type { Props } from './TeiSearchForm.types'; import { withGotoInterface } from '../../FormFields/New'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); @@ -197,7 +198,7 @@ class SearchFormPlain extends React.Component, } const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) - : i18n.t('Search by attributes'); + : tCustomTerm('Search by {{attributesLabel}}', { attributesLabel: this.props.attributesLabel }); return (
{ const formValues = state.formsValues[formId] || {}; @@ -19,4 +24,6 @@ const mapStateToProps = (state: any, props: any) => { }; }; -export const TeiSearchForm = connect(mapStateToProps, () => ({}))(TeiSearchFormComponent); +export const TeiSearchForm = connect(mapStateToProps, () => ({}))( + withCustomLabels(customLabels)(TeiSearchFormComponent), +); diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts index abf5f903ab..e14653c659 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts @@ -10,4 +10,5 @@ export type Props = { searchGroup: SearchGroup; attributesWithValuesCount: number; formsValues: { [formElement: string]: any }; + attributesLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index fbfcd015a9..522b34e730 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -26,6 +26,8 @@ import { useDataEntryFormConfig, } from '../DataEntries/common/TEIAndEnrollment'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; const styles: Readonly = { header: { @@ -80,6 +82,7 @@ const WidgetProfilePlain = ({ const queryClient = useQueryClient(); const [open, setOpenStatus] = useState(true); const [modalState, setTeiModalState] = useState(TEI_MODAL_STATE.CLOSE); + const attributesLabel = useTermLabel('attribute', { plural: true }); const { loading: programsLoading, program, error: programsError } = useProgram(programId); const { storedAttributeValues, storedGeometry, hasError } = useSelector(({ trackedEntityInstance }: any) => ({ storedAttributeValues: trackedEntityInstance?.attributeValues, @@ -176,11 +179,11 @@ const WidgetProfilePlain = ({

{trackedEntityTypeName - ? i18n.t('No attributes configured for {{trackedEntityTypeName}}', { + ? tCustomTerm('No {{attributesLabel}} configured for {{trackedEntityTypeName}}', { + attributesLabel, trackedEntityTypeName, - interpolation: { escapeValue: false }, }) - : i18n.t('No attributes configured')} + : tCustomTerm('No {{attributesLabel}} configured', { attributesLabel })}

); diff --git a/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx b/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx index 3e56259858..0e6a1aeed0 100644 --- a/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx +++ b/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx @@ -3,6 +3,8 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { NoticeBox, spacers } from '@dhis2/ui'; import type { SearchAttribute } from '../../../metaDataMemoryStoreBuilders/common/factory/searchGroup'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../tCustomTerm'; const styles: Readonly = (theme: any) => ({ container: { @@ -22,6 +24,7 @@ const UnsupportedAttributesNotificationPlain = ({ unsupportedAttributes, classes, }: Props) => { + const attributesLabel = useTermLabel('attribute', { plural: true }); const message = i18n.t('The following attribute type is not supported for searching and has been hidden', { count: unsupportedAttributes.length, @@ -33,7 +36,7 @@ const UnsupportedAttributesNotificationPlain = ({ return (
- + {message}{': '} {unsupportedAttributes.map((attr, index) => ( From 7c2aeb629dcea1b6d1301df7038c8393929a41f7 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:10:33 +0000 Subject: [PATCH 042/118] feat: replace "field" with custom terminology for attribute labels --- i18n/en.pot | 17 ++--------- .../NotEnoughAttributesMessage.ts | 28 +++++++++---------- .../SearchStatus/SearchStatus.component.tsx | 3 ++ 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 1ca6283c80..ce75f4cfe8 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T11:01:18.198Z\n" -"PO-Revision-Date: 2026-08-31T11:01:18.198Z\n" +"POT-Creation-Date: 2026-08-31T11:10:35.292Z\n" +"PO-Revision-Date: 2026-08-31T11:10:35.292Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1175,19 +1175,6 @@ msgstr "If none of search results match, you can create a new {{trackedEntityNam msgid "Create new" msgstr "Create new" -msgid "Fill in these fields to search{{escape}} {{ searchableAttributes }}" -msgstr "Fill in these fields to search{{escape}} {{ searchableAttributes }}" - -msgid "" -"Fill in at least {{minAttributesRequiredToSearch}} of these fields to " -"search{{escape}} {{searchableAttributes}}" -msgstr "" -"Fill in at least {{minAttributesRequiredToSearch}} of these fields to " -"search{{escape}} {{searchableAttributes}}" - -msgid "Fill in this field to search{{escape}} {{searchableAttributes}}" -msgstr "Fill in this field to search{{escape}} {{searchableAttributes}}" - msgid "" "You can change your search terms and search again to find what you are " "looking for." diff --git a/src/core_modules/capture-core/components/SearchBox/SearchStatus/NotEnoughAttributesMessage.ts b/src/core_modules/capture-core/components/SearchBox/SearchStatus/NotEnoughAttributesMessage.ts index e298c1e54a..9c14c5102c 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchStatus/NotEnoughAttributesMessage.ts +++ b/src/core_modules/capture-core/components/SearchBox/SearchStatus/NotEnoughAttributesMessage.ts @@ -1,41 +1,41 @@ -import i18n from '@dhis2/d2-i18n'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const NotEnoughAttributesMessage = ({ minAttributesRequiredToSearch, searchableFields, + attributesLabel, + attributeLabel, }: { minAttributesRequiredToSearch: number; searchableFields: Array>; + attributesLabel: string; + attributeLabel: string; }) => { const searchableFieldsDisplayname = searchableFields?.map((field: any) => field.formName)?.join(', '); if (minAttributesRequiredToSearch === searchableFields.length && searchableFields.length > 1) { - return i18n.t('Fill in these fields to search{{escape}} {{ searchableAttributes }}', { + return tCustomTerm('Fill in these {{attributesLabel}} to search{{escape}} {{ searchableAttributes }}', { escape: ':', + attributesLabel, searchableAttributes: searchableFieldsDisplayname, - interpolation: { - escapeValue: false, - }, }); } if (searchableFields.length > 1) { - // eslint-disable-next-line max-len - return i18n.t('Fill in at least {{minAttributesRequiredToSearch}} of these fields to search{{escape}} {{searchableAttributes}}', + return tCustomTerm( + // eslint-disable-next-line max-len + 'Fill in at least {{minAttributesRequiredToSearch}} of these {{attributesLabel}} to search{{escape}} {{searchableAttributes}}', { escape: ':', minAttributesRequiredToSearch, + attributesLabel, searchableAttributes: searchableFieldsDisplayname, - interpolation: { - escapeValue: false, - }, }, ); } - return i18n.t('Fill in this field to search{{escape}} {{searchableAttributes}}', { + return tCustomTerm('Fill in this {{attributeLabel}} to search{{escape}} {{searchableAttributes}}', { escape: ':', + attributeLabel, searchableAttributes: searchableFieldsDisplayname, - interpolation: { - escapeValue: false, - }, }); }; + diff --git a/src/core_modules/capture-core/components/SearchBox/SearchStatus/SearchStatus.component.tsx b/src/core_modules/capture-core/components/SearchBox/SearchStatus/SearchStatus.component.tsx index 3c468c271f..cbb1d35cea 100644 --- a/src/core_modules/capture-core/components/SearchBox/SearchStatus/SearchStatus.component.tsx +++ b/src/core_modules/capture-core/components/SearchBox/SearchStatus/SearchStatus.component.tsx @@ -44,6 +44,7 @@ export const SearchStatusPlain = ({ trackedEntityName, classes, }: ComponentProps & WithStyles) => { + const attributeLabel = useTermLabel('attribute'); const attributesLabel = useTermLabel('attribute', { plural: true }); if (searchStatus === searchBoxStatus.SHOW_RESULTS) { return ; @@ -120,6 +121,8 @@ export const SearchStatusPlain = ({ From 3ead8dd2aa9d1477857d01c04a7a5fe9e3525b6a Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:15:49 +0000 Subject: [PATCH 043/118] fix: simplify interpolation handling in tCustomTerm function --- i18n/en.pot | 4 ++-- src/core_modules/capture-core/utils/tCustomTerm.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 70ea1516ce..b4c8422fb7 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T09:17:40.273Z\n" -"PO-Revision-Date: 2026-08-31T09:17:40.273Z\n" +"POT-Creation-Date: 2026-08-31T11:15:51.335Z\n" +"PO-Revision-Date: 2026-08-31T11:15:51.335Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/utils/tCustomTerm.ts b/src/core_modules/capture-core/utils/tCustomTerm.ts index 1e9ceb81c0..a1f46accbf 100644 --- a/src/core_modules/capture-core/utils/tCustomTerm.ts +++ b/src/core_modules/capture-core/utils/tCustomTerm.ts @@ -32,6 +32,6 @@ export const tCustomTerm = (key: string, options: Options = {}): string => { return i18n.t(key, { ...casedValues, - interpolation: { escapeValue: false, ...(interpolation ?? {}) }, + interpolation: { escapeValue: false, ...interpolation }, }); }; From 531b87ae2a0fad4aa72f44a148dd40aed11f908c Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:34:11 +0000 Subject: [PATCH 044/118] feat: follow-up labels singular --- i18n/en.pot | 16 ++-------------- .../Actions/Followup/Followup.component.tsx | 15 +++++++++------ .../WidgetEnrollment.component.tsx | 3 ++- .../Setup/hooks/useFiltersOnly.ts | 6 ++++-- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 1ae33df24c..094558eaec 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T11:23:59.337Z\n" -"PO-Revision-Date: 2026-08-31T11:23:59.337Z\n" +"POT-Creation-Date: 2026-08-31T11:34:13.748Z\n" +"PO-Revision-Date: 2026-08-31T11:34:13.748Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1214,12 +1214,6 @@ msgstr "Mark as cancelled" msgid "Mark incomplete" msgstr "Mark incomplete" -msgid "Remove mark for follow-up" -msgstr "Remove mark for follow-up" - -msgid "Mark for follow-up" -msgstr "Mark for follow-up" - msgid "Transfer" msgstr "Transfer" @@ -1269,9 +1263,6 @@ msgstr "" msgid "Incident date" msgstr "Incident date" -msgid "Follow-up" -msgstr "Follow-up" - msgid "Started at{{escape}}" msgstr "Started at{{escape}}" @@ -1798,9 +1789,6 @@ msgstr "Owner organisation unit" msgid "Registration Date" msgstr "Registration Date" -msgid "Follow up" -msgstr "Follow up" - msgid "Delete {{count}} {{ trackedEntityName }}" msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/Followup.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/Followup.component.tsx index a4540315f4..e9cf4619a6 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/Followup.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/Followup.component.tsx @@ -1,10 +1,12 @@ import React from 'react'; import { IconFlag16, MenuItem } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import type { Props } from './followup.types'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; -export const Followup = ({ enrollment, onUpdate }: Props) => - (enrollment.followUp ? ( +export const Followup = ({ enrollment, onUpdate }: Props) => { + const followUpLabel = useTermLabel('followUp'); + return enrollment.followUp ? ( }) } icon={} - label={i18n.t('Remove mark for follow-up')} + label={tCustomTerm('Remove mark for {{followUpLabel}}', { followUpLabel })} suffix="" /> ) : ( @@ -29,7 +31,8 @@ export const Followup = ({ enrollment, onUpdate }: Props) => }) } icon={} - label={i18n.t('Mark for follow-up')} + label={tCustomTerm('Mark for {{followUpLabel}}', { followUpLabel })} suffix="" /> - )); + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 83b34ddfd1..86e335727c 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -86,6 +86,7 @@ const WidgetEnrollmentPlain = ({ }: PlainProps & WithStyles) => { const { programWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); const enrollmentLabel = useTermLabel('enrollment'); + const followUpLabel = useTermLabel('followUp'); const enrollmentReadOnly = readOnlyMode || !programWriteAccess; const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); @@ -133,7 +134,7 @@ const WidgetEnrollmentPlain = ({
{enrollment.followUp && ( - {i18n.t('Follow-up')} + {capitalizeFirstLetter(followUpLabel)} )} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts index c80f59e303..b1aacf00a6 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import { featureAvailable, FEATURES } from 'capture-core-utils'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import i18n from '@dhis2/d2-i18n'; import { dataElementTypes, type TrackerProgram, useTermLabel } from '../../../../../metaData'; import { tCustomTerm } from '../../../../../utils/tCustomTerm'; @@ -10,6 +11,7 @@ export const useFiltersOnly = ( programStageId?: string, ) => { const enrollmentLabel = useTermLabel('enrollment'); + const followUpLabel = useTermLabel('followUp'); return useMemo(() => { const enableUserAssignment = !programStageId && Array.from(stages.values()).find((stage: any) => stage.enableUserAssignment); @@ -73,7 +75,7 @@ export const useFiltersOnly = ( { id: MAIN_FILTERS.FOLLOW_UP, type: dataElementTypes.BOOLEAN, - header: i18n.t('Follow up'), + header: capitalizeFirstLetter(followUpLabel), showInMoreFilters: true, multiValueFilter: false, transformRecordsFilter: (rawFilter: string) => ({ @@ -98,5 +100,5 @@ export const useFiltersOnly = ( ] : []), ]; - }, [enrollmentDateLabel, incidentDateLabel, showIncidentDate, stages, programStageId, enrollmentLabel]); + }, [enrollmentDateLabel, incidentDateLabel, showIncidentDate, stages, programStageId, enrollmentLabel, followUpLabel]); }; From 09dc6c0e44ee3d3f0813a1dc79895ff2e1ef25be Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:23:00 +0000 Subject: [PATCH 045/118] feat: organisation unit labels singular --- i18n/en.pot | 88 ++----------------- .../CardList/CardListItem.component.tsx | 4 +- .../DataEntry/DataEntry.component.tsx | 4 +- .../DataEntry/DataEntry.container.ts | 9 +- .../epics/newEventDataEntry.epics.ts | 7 +- .../orgUnit.validatorContainersGetter.ts | 17 ++-- .../helpers/getOpenDataEntryActions.ts | 7 +- ...SingleEventRegistrationEntry.container.tsx | 4 +- .../SingleOrgUnitSelectField.component.tsx | 14 ++- .../LockedSelector/LockedSelector.epics.ts | 14 ++- .../Enrollment/MissingMessage.component.tsx | 7 +- ...idCategoryCombinationForOrgUnitMessage.tsx | 26 ++++-- .../NoSelectionsInfoBox.tsx | 66 ++++++++------ .../WithoutOrgUnitSelectedMessage.tsx | 8 +- .../Pages/New/NewPage.component.tsx | 10 ++- .../RegUnitSelector.component.tsx | 14 ++- .../Pages/ViewEvent/epics/viewEvent.epics.ts | 11 ++- .../RegUnitSelector.component.tsx | 13 ++- .../RegUnitSelector/RegUnitSelector.types.ts | 1 + .../SearchOrgUnitSelector.component.tsx | 13 +-- .../SearchOrgUnitSelector.container.ts | 14 +-- .../WidgetEventEditWrapper.tsx | 3 +- .../OrgUnitSelector.component.tsx | 22 +++-- .../QuickSelector/Program/ProgramList.tsx | 8 +- .../SearchOrgUnitSelector.component.tsx | 12 +-- .../SearchOrgUnitSelector.container.ts | 14 +-- .../SearchOrgUnitSelector.types.ts | 1 + .../TransferModal/TransferModal.component.tsx | 8 +- .../DataEntry/DataEntry.component.tsx | 4 +- .../DataEntry/DataEntry.container.tsx | 9 +- .../orgUnit.validatorContainersGetter.ts | 17 ++-- .../helpers/getOpenDataEntryActions.ts | 14 ++- .../OrgUnitFetcher.component.tsx | 6 +- .../Validated/useLifecycle.ts | 2 +- .../DataEntry/editEventDataEntry.actions.ts | 4 +- .../orgUnit.validatorContainersGetter.ts | 17 ++-- .../EditEventDataEntry.component.tsx | 4 +- .../EditEventDataEntry.container.ts | 9 +- .../editEventDataEntry.epics.ts | 9 +- .../ViewEventDataEntry.component.tsx | 2 +- .../ViewEventDataEntry.container.ts | 6 +- .../viewEventDataEntry.actions.ts | 2 +- .../ScheduleOrgUnit.component.tsx | 9 +- .../EnterData.component.tsx | 13 +-- .../OrgUnitSelectorForRelatedStages.tsx | 6 +- .../RelatedStagesActions.container.tsx | 5 +- .../ValidationFunctions.ts | 9 +- .../relatedStageEventIsValid.ts | 2 + .../relatedStageEventIsValid.types.ts | 1 + .../Stage/StageDetail/hooks/useEventList.ts | 10 ++- .../WidgetTwoEventWorkspace.component.tsx | 6 +- .../utils/getDataEntryDetails.ts | 4 +- .../useDefaultColumnConfig.ts | 17 ++-- .../Setup/hooks/useDefaultColumnConfig.ts | 23 ++--- .../metaData/helpers/customLabels.ts | 2 +- .../coreOrgUnit/useCoreOrgUnit.tsx | 16 ++-- 56 files changed, 368 insertions(+), 279 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 094558eaec..283379e212 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T11:34:13.748Z\n" -"PO-Revision-Date: 2026-08-31T11:34:13.748Z\n" +"POT-Creation-Date: 2026-09-01T07:23:02.415Z\n" +"PO-Revision-Date: 2026-09-01T07:23:02.415Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -81,9 +81,6 @@ msgstr "Enrolled" msgid "Previously enrolled" msgstr "Previously enrolled" -msgid "Organisation unit" -msgstr "Organisation unit" - msgid "Last updated" msgstr "Last updated" @@ -222,9 +219,6 @@ msgstr "Completed" msgid "Please add or cancel the note before saving the event" msgstr "Please add or cancel the note before saving the event" -msgid "Please provide an valid organisation unit" -msgstr "Please provide an valid organisation unit" - msgid "Save and add another" msgstr "Save and add another" @@ -545,9 +539,6 @@ msgstr "Type to filter options" msgid "No match found" msgstr "No match found" -msgid "Search for an organisation unit" -msgstr "Search for an organisation unit" - msgid "Clear" msgstr "Clear" @@ -643,15 +634,9 @@ msgstr "More filters" msgid "Rows per page" msgstr "Rows per page" -msgid "Could not get organisation unit" -msgstr "Could not get organisation unit" - msgid "Program doesn't exist" msgstr "Program doesn't exist" -msgid "Selected program is invalid for selected organisation unit" -msgstr "Selected program is invalid for selected organisation unit" - msgid "Add note" msgstr "Add note" @@ -679,13 +664,6 @@ msgstr "Make referral" msgid "{{programName}} has categories. Choose all categories to view dashboard." msgstr "{{programName}} has categories. Choose all categories to view dashboard." -msgid "" -"You do not have permissions to access to this program, registering unit or " -"record, contact your administrator for more information." -msgstr "" -"You do not have permissions to access to this program, registering unit or " -"record, contact your administrator for more information." - msgid "{{teiDisplayName}} is not enrolled in this program." msgstr "{{teiDisplayName}} is not enrolled in this program." @@ -737,9 +715,6 @@ msgstr "You can't add any more {{ programStageName }} events" msgid "Cancel without saving" msgstr "Cancel without saving" -msgid "The category option is not valid for the selected organisation unit." -msgstr "The category option is not valid for the selected organisation unit." - msgid "Please select a valid combination." msgstr "Please select a valid combination." @@ -749,13 +724,6 @@ msgstr "Get started with Capture app" msgid "Report data" msgstr "Report data" -msgid "" -"Choose a program and organisation unit to see existing data and create new " -"records." -msgstr "" -"Choose a program and organisation unit to see existing data and create new " -"records." - msgid "Click 'Search'. For program-specific results, choose a program first." msgstr "Click 'Search'. For program-specific results, choose a program first." @@ -765,12 +733,6 @@ msgstr "Learn more about Capture app" msgid "Please select {{category}}." msgstr "Please select {{category}}." -msgid "Please select an organisation unit" -msgstr "Please select an organisation unit" - -msgid "See working list without organisation unit" -msgstr "See working list without organisation unit" - msgid "Search for a {{trackedEntityName}}" msgstr "Search for a {{trackedEntityName}}" @@ -784,9 +746,6 @@ msgstr "" "You don't have access to create a {{trackedEntityName}} in the current " "selections" -msgid "Choose an organisation unit to start reporting" -msgstr "Choose an organisation unit to start reporting" - msgid "Choose the {{missingCategories}} to start reporting" msgstr "Choose the {{missingCategories}} to start reporting" @@ -826,9 +785,6 @@ msgstr "Show all" msgid "Program" msgstr "Program" -msgid "Organisation Unit" -msgstr "Organisation Unit" - msgid "Registration" msgstr "Registration" @@ -893,9 +849,6 @@ msgstr "Event could not be loaded. Are you sure it exists?" msgid "Event could not be loaded" msgstr "Event could not be loaded" -msgid "Organisation unit could not be loaded" -msgstr "Organisation unit could not be loaded" - msgid "tracked entity instance" msgstr "tracked entity instance" @@ -905,12 +858,6 @@ msgstr "All accessible" msgid "Selected" msgstr "Selected" -msgid "Please select an organisation unit." -msgstr "Please select an organisation unit." - -msgid "Organisation unit scope" -msgstr "Organisation unit scope" - msgid "Selected program" msgstr "Selected program" @@ -997,15 +944,9 @@ msgstr "Add relationship" msgid "No results found for " msgstr "No results found for " -msgid "Choose an organisation unit in the form below" -msgstr "Choose an organisation unit in the form below" - msgid "None selected" msgstr "None selected" -msgid "Choose an organisation unit" -msgstr "Choose an organisation unit" - msgid "Choose a {{categoryName}}" msgstr "Choose a {{categoryName}}" @@ -1021,9 +962,6 @@ msgstr "No programs available." msgid "Search for a program" msgstr "Search for a program" -msgid "Some programs are being filtered by the chosen organisation unit" -msgstr "Some programs are being filtered by the chosen organisation unit" - msgid "Show all programs" msgstr "Show all programs" @@ -1253,13 +1191,6 @@ msgstr "Set area" msgid "Transfer Ownership" msgstr "Transfer Ownership" -msgid "" -"Choose the organisation unit to which {{enrollmentLabel}} ownership should " -"be transferred." -msgstr "" -"Choose the organisation unit to which {{enrollmentLabel}} ownership should " -"be transferred." - msgid "Incident date" msgstr "Incident date" @@ -1278,9 +1209,6 @@ msgstr "Add coordinates" msgid "Add area" msgstr "Add area" -msgid "organisation unit could not be retrieved. Please try again later." -msgstr "organisation unit could not be retrieved. Please try again later." - msgid "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" @@ -1368,9 +1296,6 @@ msgstr[1] "" msgid "Schedule date / Due date" msgstr "Schedule date / Due date" -msgid "Please provide a valid organisation unit" -msgstr "Please provide a valid organisation unit" - msgid "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" @@ -1783,9 +1708,6 @@ msgstr "This cannot be undone." msgid "Are you sure you want to delete the selected events?" msgstr "Are you sure you want to delete the selected events?" -msgid "Owner organisation unit" -msgstr "Owner organisation unit" - msgid "Registration Date" msgstr "Registration Date" @@ -1965,6 +1887,9 @@ msgstr "Error editing the event, the changes made were not saved" msgid "Error updating the Assignee" msgstr "Error updating the Assignee" +msgid "Could not save enrollment note" +msgstr "Could not save enrollment note" + msgid "Could not save event note" msgstr "Could not save event note" @@ -1989,6 +1914,9 @@ msgstr "Please provide a valid age" msgid "Please provide a valid phone number" msgstr "Please provide a valid phone number" +msgid "Please provide a valid organisation unit" +msgstr "Please provide a valid organisation unit" + msgid "This value already exists" msgstr "This value already exists" diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx index a6a0a21b93..0108d6d6c1 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n'; import React from 'react'; import moment from 'moment'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { colors, Tag, IconCheckmark16, Tooltip } from '@dhis2/ui'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { CardImage } from 'capture-ui/CardImage/CardImage.component'; @@ -155,6 +156,7 @@ const CardListItemIndex = ({ const { orgUnitId, enrolledAt } = deriveEnrollmentOrgUnitIdAndDate(enrollments, enrollmentType, currentProgramId); const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(orgUnitId ?? null); const enrollmentLabel = useTermLabel('enrollment', { programId: currentProgramId }); + const orgUnitLabel = useTermLabel('orgUnit', { programId: currentProgramId }); const program: TrackerProgram | undefined = enrollments.length ? deriveProgramFromEnrollment(enrollments, currentSearchScopeType) : undefined; @@ -220,7 +222,7 @@ const CardListItemIndex = ({ return (<> { getComponent: () => orgUnitComponent, getComponentProps: (props: any) => createComponentProps(props, { width: props && props.formHorizontal ? 150 : 350, - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, required: true, }), getPropName: () => 'orgUnit', - getValidatorContainers: () => getOrgUnitValidatorContainers(), + getValidatorContainers: (props: any) => getOrgUnitValidatorContainers(props.orgUnitLabel), getMeta: () => ({ placement: placements.TOP, section: dataEntrySectionNames.BASICINFO, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts index 6248331077..b9362b0201 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts @@ -25,6 +25,11 @@ import { import type { RenderFoundation } from '../../../../../metaData'; import { withLoadingIndicator, withErrorMessageHandler } from '../../../../../HOC'; import { newEventSaveTypes } from './newEventSaveTypes'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; + +const customLabels = { + orgUnitLabel: { key: 'orgUnit' }, +} as const; const makeMapStateToProps = () => { const programNameSelector = makeProgramNameSelector(); @@ -111,5 +116,7 @@ const mapDispatchToProps = (dispatch: any) => ({ }); export const DataEntry = connect(makeMapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(withErrorMessageHandler()(DataEntryComponent)), + withLoadingIndicator()(withErrorMessageHandler()( + withCustomLabels(customLabels)(DataEntryComponent), + )), ); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.ts index 149dadbdca..eabac1a4ba 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/newEventDataEntry.epics.ts @@ -66,7 +66,12 @@ export const resetDataEntryForNewEventEpic = (action$: EpicAction, store: R displayName: category.name, })), } : null; - return batchActions(getOpenDataEntryActions(programCategory, selectedCategories, orgUnit)); + return batchActions(getOpenDataEntryActions( + program.id, + programCategory, + selectedCategories, + orgUnit, + )); }), ); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts index ec6fb14c72..b8c2041b54 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts @@ -1,14 +1,11 @@ import { isValidOrgUnit } from 'capture-core-utils/validators/form'; -import i18n from '@dhis2/d2-i18n'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const validateOrgUnit = (value?: any) => isValidOrgUnit(value); -export const getOrgUnitValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateOrgUnit, - errorMessage: i18n.t('Please provide an valid organisation unit'), - }, - ]; - return validatorContainers; -}; +export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ + { + validator: validateOrgUnit, + errorMessage: tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + }, +]; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts index acf0dffa97..910720a372 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts @@ -8,8 +8,9 @@ import { addFormData } from '../../../../../D2Form/actions/form.actions'; import { getCategoryOptionsValidatorContainers } from '../../../../Enrollment/fieldValidators'; import type { ProgramCategory } from '../../../../../WidgetEventSchedule/CategoryOptions/CategoryOptions.types'; import type { DataEntryPropToInclude } from '../../../../../DataEntry/actions/dataEntryLoad.utils'; +import { getTermLabel } from '../../../../../../metaData/helpers/customLabels'; -const dataEntryPropsToInclude: Array = [ +const buildDataEntryPropsToInclude = (orgUnitLabel: string): Array => [ { id: 'occurredAt', type: 'DATE', @@ -18,7 +19,7 @@ const dataEntryPropsToInclude: Array = [ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(orgUnitLabel), }, { clientId: 'geometry', @@ -49,6 +50,7 @@ const dataEntryPropsToInclude: Array = [ ]; export const getOpenDataEntryActions = ( + programId: string, programCategory?: ProgramCategory | null, selectedCategories?: { [key: string]: string } | null, orgUnit?: CoreOrgUnit | null, @@ -59,6 +61,7 @@ export const getOpenDataEntryActions = ( : undefined, }; + const dataEntryPropsToInclude = buildDataEntryPropsToInclude(getTermLabel(programId, 'orgUnit')); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ id: `attributeCategoryOptions-${category.id}`, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx index baffeaec84..fe557f923a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx @@ -44,12 +44,12 @@ const openSingleEventDataEntry = (InnerComponent: React.ComponentType ({ selectedOrgUnitContainer: { display: 'flex', @@ -107,6 +112,7 @@ type SingleOrgUnitSelectFieldProps = { disabled?: boolean; maxTreeHeight?: number; autoSelectSingleOrgUnit?: boolean; + orgUnitLabel: string; }; type Props = SingleOrgUnitSelectFieldProps & WithStyles; @@ -284,7 +290,9 @@ class SingleOrgUnitSelectFieldPlain extends React.Component, store: ReduxStore) => action$.pipe( @@ -24,14 +26,17 @@ export const getOrgUnitDataBasedOnUrlUpdateEpic = (action$: EpicAction, sto filter(action => action.payload.nextProps.orgUnitId), concatMap((action) => { const { organisationUnits } = store.value as any; - const { orgUnitId } = action.payload.nextProps; + const { orgUnitId, programId } = action.payload.nextProps; if (organisationUnits[orgUnitId]) { return of(completeUrlUpdate()); } + const orgUnitLabel = getTermLabel(programId, 'orgUnit'); return of(startLoading(), getCoreOrgUnit({ orgUnitId, onSuccess: setCurrentOrgUnitBasedOnUrl, - onError: () => errorRetrievingOrgUnitBasedOnUrl(i18n.t('Could not get organisation unit')), + onError: () => errorRetrievingOrgUnitBasedOnUrl( + tCustomTerm('Could not get {{orgUnitLabel}}', { orgUnitLabel }), + ), })); }), ); @@ -63,7 +68,10 @@ export const validateSelectionsBasedOnUrlUpdateEpic = (action$: EpicAction) } if (orgUnitId && !program.organisationUnits[orgUnitId]) { - return invalidSelectionsFromUrl(i18n.t('Selected program is invalid for selected organisation unit')); + const orgUnitLabel = getTermLabel(programId, 'orgUnit'); + return invalidSelectionsFromUrl( + tCustomTerm('Selected program is invalid for selected {{orgUnitLabel}}', { orgUnitLabel }), + ); } } diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index 103387e926..fd35cef593 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -187,6 +187,7 @@ const MissingMessagePlain = ({ const { programId, teiId, enrollmentId } = useLocationQuery(); const enrollmentLabel = useTermLabel('enrollment'); const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); + const orgUnitLabel = useTermLabel('orgUnit'); const { trackedEntityName: tetName } = useScopeInfo(tetId); const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId); @@ -253,7 +254,11 @@ const MissingMessagePlain = ({ missingStatus === missingStatuses.RESTRICTED_PROGRAM_NO_ACCESS && {/* eslint-disable-next-line max-len */} - {i18n.t('You do not have permissions to access to this program, registering unit or record, contact your administrator for more information.')} + {tCustomTerm( + // eslint-disable-next-line max-len + 'You do not have permissions to access to this program, {{orgUnitLabel}} or record, contact your administrator for more information.', + { orgUnitLabel }, + )} } diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.tsx index f9032b3355..3828976537 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.tsx @@ -2,6 +2,8 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { IncompleteSelectionsMessage } from '../../../../IncompleteSelectionsMessage'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const styles: Readonly = { incompleteMessageContainer: { @@ -11,15 +13,21 @@ const styles: Readonly = { type Props = WithStyles; -export const InvalidCategoryCombinationForOrgUnitMessagePlain = ({ classes }: Props) => ( -
- - {i18n.t('The category option is not valid for the selected organisation unit.')} - {' '} - {i18n.t('Please select a valid combination.')} - -
-); +export const InvalidCategoryCombinationForOrgUnitMessagePlain = ({ classes }: Props) => { + const orgUnitLabel = useTermLabel('orgUnit'); + return ( +
+ + {tCustomTerm( + 'The category option is not valid for the selected {{orgUnitLabel}}.', + { orgUnitLabel }, + )} + {' '} + {i18n.t('Please select a valid combination.')} + +
+ ); +}; export const InvalidCategoryCombinationForOrgUnitMessage = withStyles(styles)( InvalidCategoryCombinationForOrgUnitMessagePlain, diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx index 1284044849..f839c82a95 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx @@ -2,6 +2,8 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { colors } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const styles: Readonly = { container: { @@ -63,36 +65,42 @@ const EmptyStateIcon = () => ( const documentationLink = 'https://docs.dhis2.org/en/use/user-guides/dhis-core-version-master/tracking-individual-level-data/capture.html'; -const NoSelectionsInfoBoxPlain = ({ classes }: Props) => ( -
-
-
- -
-

- {i18n.t('Get started with Capture app')} -

-
- - {i18n.t('Report data')}:{' '} - {i18n.t('Choose a program and organisation unit to see existing data and create new records.')} - - - {i18n.t('Search')}:{' '} - {i18n.t('Click \'Search\'. For program-specific results, choose a program first.')} - -
+const NoSelectionsInfoBoxPlain = ({ classes }: Props) => { + const orgUnitLabel = useTermLabel('orgUnit'); + return ( +
+
+
+ +
+

+ {i18n.t('Get started with Capture app')} +

+
+ + {i18n.t('Report data')}:{' '} + {tCustomTerm( + 'Choose a program and {{orgUnitLabel}} to see existing data and create new records.', + { orgUnitLabel }, + )} + + + {i18n.t('Search')}:{' '} + {i18n.t('Click \'Search\'. For program-specific results, choose a program first.')} + +
- - {i18n.t('Learn more about Capture app')} - + + {i18n.t('Learn more about Capture app')} + +
-
-); + ); +}; export const NoSelectionsInfoBox = withStyles(styles)(NoSelectionsInfoBoxPlain); diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx index cf5e3f0644..dc708fdca8 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx @@ -3,8 +3,9 @@ import { colors } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { IncompleteSelectionsMessage } from '../../../../IncompleteSelectionsMessage'; -import { programTypes, TrackerProgram } from '../../../../../metaData'; +import { programTypes, TrackerProgram, useTermLabel } from '../../../../../metaData'; import { useProgramInfo } from '../../../../../hooks/useProgramInfo'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const styles: Readonly = { incompleteMessageContainer: { @@ -52,6 +53,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ }: Props) => { const { program, programType } = useProgramInfo(programId); const isTracker = programType === programTypes.TRACKER_PROGRAM; + const orgUnitLabel = useTermLabel('orgUnit'); const trackedEntityName = program instanceof TrackerProgram ? program.trackedEntityType?.name @@ -66,7 +68,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ >
- {i18n.t('Please select an organisation unit')} + {tCustomTerm('Please select an {{orgUnitLabel}}', { orgUnitLabel })}
{showWorkingListLink && ( )} {showSearchLink && ( diff --git a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx index 686c9b99b6..ebe623f659 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx @@ -14,6 +14,8 @@ import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { RegistrationDataEntry } from './RegistrationDataEntry'; import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const styles: Readonly = { container: { @@ -69,6 +71,7 @@ const NewPagePlain = ({ showMessageThatCategoryOptionIsInvalidForOrgUnit, ]); const orgUnitId = useSelector(({ currentSelections }: any) => currentSelections.orgUnitId); + const orgUnitLabel = useTermLabel('orgUnit'); return (
@@ -101,7 +104,7 @@ const NewPagePlain = ({ newPageStatus === newPageStatuses.WITHOUT_ORG_UNIT_SELECTED && <> - {i18n.t('Choose an organisation unit to start reporting')} + {tCustomTerm('Choose an {{orgUnitLabel}} to start reporting', { orgUnitLabel })} diff --git a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx index 7eb9c2440d..2c95028e88 100644 --- a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx +++ b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { SelectionBoxes, withDefaultFieldContainer, @@ -10,6 +11,7 @@ import { SingleOrgUnitSelectField, } from '../../FormFields/New'; import type { SearchOrgUnitSelectorProps } from './SearchOrgUnitSelector.types'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const TeiSearchOrgUnitField = withFocusSaver()( withCalculateMessages()( @@ -65,11 +67,11 @@ export class SearchOrgUnitSelector extends React.Component { - const { selectedOrgUnitScope } = this.props; + const { selectedOrgUnitScope, orgUnitLabel } = this.props; return ( { if (!this.isValid() && this.props.searchAttempted) { - return i18n.t('Please select an organisation unit.'); + return tCustomTerm('Please select an {{orgUnitLabel}}.', { orgUnitLabel: this.props.orgUnitLabel }); } return null; } @@ -111,10 +113,10 @@ export class SearchOrgUnitSelector extends React.Component { - const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText } = this.props; + const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText, orgUnitLabel } = this.props; return ( { const searchId = props.searchId; + const teiSearch = state.teiSearch[searchId]; + const programId = teiSearch.selectedProgramId; const filteredRoots = getOrgUnitRoots(searchId); const roots = filteredRoots || getOrgUnitRoots('searchRoots'); return { - selectedOrgUnit: state.teiSearch[searchId].selectedOrgUnit, - selectedOrgUnitScope: state.teiSearch[searchId].selectedOrgUnitScope, + selectedOrgUnit: teiSearch.selectedOrgUnit, + selectedOrgUnitScope: teiSearch.selectedOrgUnitScope, treeRoots: roots, - treeSearchText: state.teiSearch[searchId].orgUnitsSearchText, - treeReady: !state.teiSearch[searchId].orgUnitsLoading, - treeKey: state.teiSearch[searchId].orgUnitsSearchText || 'initial', + treeSearchText: teiSearch.orgUnitsSearchText, + treeReady: !teiSearch.orgUnitsLoading, + treeKey: teiSearch.orgUnitsSearchText || 'initial', + orgUnitLabel: getTermLabel(programId, 'orgUnit'), }; }; diff --git a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts index 8a5222b7f2..9c3a4b18c1 100644 --- a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts +++ b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts @@ -10,4 +10,5 @@ export type SearchOrgUnitSelectorProps = { onSetOrgUnit: (searchId: string, orgUnit?: any) => void; onFilterOrgUnits: (searchId: string, searchText?: string) => void; searchAttempted?: boolean; + orgUnitLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx index 6952202814..c105dc3ee9 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx @@ -13,6 +13,7 @@ import { OrgUnitField } from './OrgUnitField'; import { useTransferValidation } from './hooks/useTransferValidation'; import { InfoBoxes } from './InfoBoxes'; import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const TransferModal = ({ enrollment, @@ -22,6 +23,7 @@ export const TransferModal = ({ isTransferLoading, }: TransferModalProps) => { const enrollmentLabel = useTermLabel('enrollment', { programId: enrollment.program }); + const orgUnitLabel = useTermLabel('orgUnit', { programId: enrollment.program }); const { selectedOrgUnit, handleOrgUnitChange, @@ -53,9 +55,9 @@ export const TransferModal = ({
- {i18n.t( - 'Choose the organisation unit to which {{enrollmentLabel}} ownership should be transferred.', - { enrollmentLabel }, + {tCustomTerm( + 'Choose the {{orgUnitLabel}} to which {{enrollmentLabel}} ownership should be transferred.', + { orgUnitLabel, enrollmentLabel }, )}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx index 411a31e2e6..84214f7804 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx @@ -198,11 +198,11 @@ const buildOrgUnitSettingsFn = () => { getComponent: () => orgUnitComponent, getComponentProps: (props: any) => createComponentProps(props, { width: props && props.formHorizontal ? 150 : 350, - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, required: true, }), getPropName: () => 'orgUnit', - getValidatorContainers: () => getOrgUnitValidatorContainers(), + getValidatorContainers: (props: any) => getOrgUnitValidatorContainers(props.orgUnitLabel), getMeta: () => ({ placement: placements.TOP, section: dataEntrySectionNames.BASICINFO, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx index 5b1f9e9a42..c7a9ae88c3 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx @@ -15,6 +15,13 @@ import { import type { AddEventSaveType } from './addEventSaveTypes'; import type { ContainerProps } from './dataEntry.types'; import { useProgramExpiryForUser } from '../../../hooks'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; + +const customLabels = { + orgUnitLabel: { key: 'orgUnit' }, +} as const; + +const WrappedDataEntryComponent = withCustomLabels(customLabels)(DataEntryComponent); export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...passOnProps }: ContainerProps) => { const dispatch = useDispatch(); @@ -77,7 +84,7 @@ export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...pa dispatch(setNewEventSaveTypes(newSaveTypes)); }, [dispatch]); return ( - isValidOrgUnit(value); -export const getOrgUnitValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateOrgUnit, - errorMessage: i18n.t('Please provide an valid organisation unit'), - }, - ]; - return validatorContainers; -}; +export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ + { + validator: validateOrgUnit, + errorMessage: tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + }, +]; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts index ba320d2c79..decdd919af 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts @@ -5,8 +5,9 @@ import { getNoteValidatorContainers } from '../fieldValidators/note.validatorCon import type { ProgramCategory } from '../../../WidgetEventSchedule/CategoryOptions/CategoryOptions.types'; import { getCategoryOptionsValidatorContainers } from '../fieldValidators/categoryOptions.validatorContainersGetter'; import type { DataEntryPropToInclude } from '../../../DataEntry/actions/dataEntryLoad.utils'; +import { getTermLabel } from '../../../../metaData/helpers/customLabels'; -const dataEntryPropsToInclude: Array = [ +const buildDataEntryPropsToInclude = (orgUnitLabel: string): Array => [ { id: 'occurredAt', type: 'DATE', @@ -15,7 +16,7 @@ const dataEntryPropsToInclude: Array = [ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(orgUnitLabel), }, { id: 'scheduledAt', @@ -40,12 +41,19 @@ const dataEntryPropsToInclude: Array = [ ]; export const getOpenDataEntryActions = - (dataEntryId: string, itemId: string, programCategory?: ProgramCategory, orgUnit?: Record) => { + ( + dataEntryId: string, + itemId: string, + programId: string, + programCategory?: ProgramCategory, + orgUnit?: Record, + ) => { const defaultDataEntryValues = { orgUnit: orgUnit ? { id: orgUnit.id, name: orgUnit.name, path: orgUnit.path } : undefined, }; + const dataEntryPropsToInclude = buildDataEntryPropsToInclude(getTermLabel(programId, 'orgUnit')); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ id: `attributeCategoryOptions-${category.id}`, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx index bf5f1d4d89..d136bdac1d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx @@ -1,19 +1,21 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit'; import { Validated } from '../Validated/Validated.container'; import type { OrgUnitFetcherProps } from './orgUnitFetcher.types'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const OrgUnitFetcher = ({ orgUnitId, ...passOnProps }: OrgUnitFetcherProps) => { const { error, orgUnit } = useCoreOrgUnit(orgUnitId); + const orgUnitLabel = useTermLabel('orgUnit'); if (error) { return (
- {i18n.t('organisation unit could not be retrieved. Please try again later.')} + {tCustomTerm('{{orgUnitLabel}} could not be retrieved. Please try again later.', { orgUnitLabel })}
); } diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts index 9768ffca43..c41bce14fe 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts @@ -38,7 +38,7 @@ export const useLifecycle = ({ useEffect(() => { if (!isLoading) { dispatch(batchActions([ - ...getOpenDataEntryActions(dataEntryId, itemId, programCategory, orgUnitContext), + ...getOpenDataEntryActions(dataEntryId, itemId, program.id, programCategory, orgUnitContext), ])); dataEntryReadyRef.current = true; delayRulesExecutionRef.current = true; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts index c081432dd8..b0948c7904 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts @@ -99,7 +99,7 @@ export const openEventForEditInDataEntry = ({ }, orgUnit: OrgUnit, foundation?: RenderFoundation, - program: Program | EventProgram | TrackerProgram | null, + program: Program | EventProgram | TrackerProgram, dataEntryId: string, dataEntryKey: string, enrollment?: EnrollmentData, @@ -119,7 +119,7 @@ export const openEventForEditInDataEntry = ({ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(getTermLabel(program.id, 'orgUnit')), }, { clientId: 'geometry', diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts index 7513b5c7b7..0e57cac6b5 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts @@ -1,14 +1,11 @@ import { isValidOrgUnit } from 'capture-core-utils/validators/form'; -import i18n from '@dhis2/d2-i18n'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const validateOrgUnit = (value?: Record) => isValidOrgUnit(value); -export const getOrgUnitValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateOrgUnit, - errorMessage: i18n.t('Please provide an valid organisation unit'), - }, - ]; - return validatorContainers; -}; +export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ + { + validator: validateOrgUnit, + errorMessage: tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + }, +]; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx index a14687d4aa..e4d69d78c0 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx @@ -225,11 +225,11 @@ const buildOrgUnitSettingsFn = () => { getComponent: () => orgUnitComponent, getComponentProps: (props: any) => createComponentProps(props, { width: props && props.formHorizontal ? 150 : 350, - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, required: true, }), getPropName: () => 'orgUnit', - getValidatorContainers: () => getOrgUnitValidatorContainers(), + getValidatorContainers: (props: any) => getOrgUnitValidatorContainers(props.orgUnitLabel), getMeta: () => ({ placement: placements.TOP, section: dataEntrySectionNames.BASICINFO, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts index e3accd78a5..d1a20bd242 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts @@ -7,13 +7,13 @@ import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import type { ReduxAction } from 'capture-core-utils/types'; import { EditEventDataEntryComponent } from './EditEventDataEntry.component'; import { withLoadingIndicator } from '../../../HOC/withLoadingIndicator'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; import { startAsyncUpdateFieldForEditEvent, startRunRulesOnUpdateForEditSingleEvent, batchActionTypes, } from '../DataEntry/editEventDataEntry.actions'; import type { RenderFoundation } from '../../../metaData'; - import { setCurrentDataEntry, startRunRulesPostUpdateField, } from '../../DataEntry/actions/dataEntry.actions'; @@ -24,9 +24,12 @@ import { startCreateNewAfterCompleting, requestSaveAndCompleteEnrollment, } from './editEventDataEntry.actions'; - import { getLocationQuery } from '../../../utils/routing/getLocationQuery'; +const customLabels = { + orgUnitLabel: { key: 'orgUnit' }, +} as const; + const mapStateToProps = (state: any, props: any) => { const eventDetailsSection = state.viewEventPage.eventDetailsSection || {}; const itemId = state.dataEntries[props.dataEntryId] && state.dataEntries[props.dataEntryId].itemId; @@ -158,5 +161,5 @@ const mapDispatchToProps = (dispatch: any, props: any): any => ({ }); export const EditEventDataEntry = connect(mapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(EditEventDataEntryComponent), + withLoadingIndicator()(withCustomLabels(customLabels)(EditEventDataEntryComponent)), ); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts index dc613b0aa5..26d581e641 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts @@ -55,8 +55,8 @@ export const loadEditEventDataEntryEpic = (action$: any, store: ReduxStore) => const loadedValues = state.viewEventPage.loadedValues; const eventContainer = loadedValues.eventContainer; const metadataContainer = getProgramAndStageFromEvent(eventContainer.event); - if (metadataContainer.error) { - return prerequisitesErrorLoadingEditEventDataEntry(metadataContainer.error); + if (metadataContainer.error || !metadataContainer.program) { + return prerequisitesErrorLoadingEditEventDataEntry(metadataContainer.error ?? ''); } const program = metadataContainer.program; @@ -208,15 +208,16 @@ export const saveEditedEventFailedEpic = (action$: any, store: any) => return batchActions(actions, batchActionTypes.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED); })); -export const requestDeleteEventDataEntryEpic = (action$: any, store: ReduxStore, dependencies: any) => +export const requestDeleteEventDataEntryEpic = (action$: any, store: any, dependencies: any) => action$.pipe( ofType(actionTypes.REQUEST_DELETE_EVENT_DATA_ENTRY), map((action: any) => { const { eventId, enrollmentId } = action.payload; const params = { enrollmentId }; const serverData = { events: [{ event: eventId }] }; + const { programId } = store.value.enrollmentPage; dependencies.navigate(`/enrollment?${buildUrlQueryString(params)}`); - return startDeleteEventDataEntry(serverData, eventId, params); + return startDeleteEventDataEntry(serverData, eventId, params, programId); })); export const startCreateNewAfterCompletingEpic = ( diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx index 42edee90ce..b058ebe9c4 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx @@ -147,7 +147,7 @@ const buildOrgUnitSettingsFn = () => { const orgUnitSettings = { getComponent: () => viewModeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, valueConverter: value => dataElement.convertValue(value, valueConvertFn), }), getPropName: () => 'orgUnit', diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts index c9ecd0d685..6217b64700 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts @@ -1,7 +1,11 @@ import { connect } from 'react-redux'; import { ViewEventDataEntryComponent } from './ViewEventDataEntry.component'; import { withLoadingIndicator } from '../../../HOC/withLoadingIndicator'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; +const customLabels = { + orgUnitLabel: { key: 'orgUnit' }, +} as const; const mapStateToProps = (state: any, props: any) => { const eventDetailsSection = state.viewEventPage.eventDetailsSection || {}; @@ -20,5 +24,5 @@ const mapStateToProps = (state: any, props: any) => { const mapDispatchToProps = (): any => ({}); export const ViewEventDataEntry = connect(mapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(ViewEventDataEntryComponent), + withLoadingIndicator()(withCustomLabels(customLabels)(ViewEventDataEntryComponent)), ); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts index eb427ebb6e..5be5e89490 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts @@ -73,7 +73,7 @@ export const loadViewEventDataEntry = { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(getTermLabel(program.id, 'orgUnit')), }, { clientId: 'geometry', diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.tsx index ab53716941..6ca18edf3c 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleOrgUnit/ScheduleOrgUnit.component.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { isValidOrgUnit } from 'capture-core-utils/validators/form'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; import { @@ -10,6 +10,8 @@ import { withInternalChangeHandler, withLabel, } from '../../FormFields/New'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; type OrgUnitValue = { checked: boolean; @@ -45,6 +47,7 @@ export const ScheduleOrgUnit = ({ orgUnit, }: Props) => { const [touched, setTouched] = useState(false); + const orgUnitLabel = useTermLabel('orgUnit'); const handleSelect = (event: any) => { setTouched(true); @@ -57,11 +60,11 @@ export const ScheduleOrgUnit = ({ }; const shouldShowError = (!isValidOrgUnit(orgUnit) && touched); - const errorMessages = i18n.t('Please provide a valid organisation unit'); + const errorMessages = tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }); return ( = { wrapper: { @@ -60,6 +61,7 @@ export const EnterDataInOrgUnitPlain = ({ errorMessages, classes, }: Props) => { + const orgUnitLabel = useTermLabel('orgUnit'); const onSelectOrgUnit = (e: { id: string; displayName: string; path: string }) => { const orgUnit = { id: e.id, @@ -93,13 +95,14 @@ export const EnterDataInOrgUnitPlain = ({
- {i18n.t( + {tCustomTerm( relatedStagesDataValues?.orgUnit?.name - ? 'Enter {{linkableStageLabel}} details for {{orgUnitLabel}} in the next step' - : 'Select organisation unit and enter {{linkableStageLabel}} details in the next step', + ? 'Enter {{linkableStageLabel}} details for {{orgUnitName}} in the next step' + : 'Select {{orgUnitLabel}} and enter {{linkableStageLabel}} details in the next step', { linkableStageLabel, - orgUnitLabel: relatedStagesDataValues?.orgUnit?.name, + orgUnitName: relatedStagesDataValues?.orgUnit?.name, + orgUnitLabel, }, )}
diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx index 54e0332b6e..e08534b327 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { SingleOrgUnitSelectField, withDefaultFieldContainer, @@ -11,6 +11,7 @@ import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; import type { ErrorMessagesForRelatedStages } from '../RelatedStagesActions'; import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; +import { useTermLabel } from '../../../metaData'; type OrgUnitValue = { checked: boolean; @@ -49,6 +50,7 @@ export const OrgUnitSelectorForRelatedStages = ({ saveAttempted, }: Props) => { const [touched, setTouched] = useState(false); + const orgUnitLabel = useTermLabel('orgUnit'); const handleSelect = (event: OrgUnitValue) => { setTouched(true); @@ -64,7 +66,7 @@ export const OrgUnitSelectorForRelatedStages = ({ return ( boolean; @@ -50,6 +51,7 @@ const RelatedStagesActionsPlain = ({ }); const { isLoading: orgUnitLoading, data } = useOrgUnitAutoSelect(); const expiryPeriod = useProgramExpiryForUser(programId); + const orgUnitLabel = useTermLabel('orgUnit'); useEffect(() => { if (!orgUnitLoading && (data as any)?.length === 1) { @@ -84,8 +86,9 @@ const RelatedStagesActionsPlain = ({ linkedEventId, expiryPeriod, setErrorMessages: addErrorMessage, + orgUnitLabel, }); - }, [relatedStageDataValues, expiryPeriod]); + }, [relatedStageDataValues, expiryPeriod, orgUnitLabel]); const getLinkedStageValues = () => ({ linkMode: relatedStageDataValues.linkMode, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts index b4f0a63c79..9189873f59 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts @@ -3,6 +3,7 @@ import { isValidOrgUnit } from 'capture-core-utils/validators/form'; import { isValidDate, isValidPeriod } from 'capture-core/utils/validation/validators/form'; import { convertFormToClient } from 'capture-core/converters'; import { dataElementTypes } from 'capture-core/metaData'; +import { tCustomTerm } from 'capture-core/utils/tCustomTerm'; import { relatedStageActions } from '../constants'; type Props = { @@ -15,6 +16,7 @@ type Props = { expiryPeriodType?: string; expiryDays?: number; }; + orgUnitLabel: string; }; export const isScheduledDateValid = ( @@ -62,6 +64,7 @@ const scheduleInOrgUnit = (props) => { orgUnit, setErrorMessages, expiryPeriod, + orgUnitLabel, } = props ?? {}; const { valid: scheduledAtIsValid, validationText } = isScheduledDateValid( scheduledAt, @@ -82,7 +85,7 @@ const scheduleInOrgUnit = (props) => { if (!orgUnitIsValid) { setErrorMessages({ - orgUnit: i18n.t('Please provide a valid organisation unit'), + orgUnit: tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), }); } else { setErrorMessages({ @@ -94,12 +97,12 @@ const scheduleInOrgUnit = (props) => { }; const enterData = (props) => { - const { orgUnit, setErrorMessages } = props ?? {}; + const { orgUnit, setErrorMessages, orgUnitLabel } = props ?? {}; const orgUnitIsValid = isValidOrgUnit(orgUnit); if (!orgUnitIsValid) { setErrorMessages({ - orgUnit: i18n.t('Please provide a valid organisation unit'), + orgUnit: tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), }); } else { setErrorMessages({ diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts index f8e5943702..4dfb26f784 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts @@ -12,6 +12,7 @@ export const relatedStageWidgetIsValid = ({ linkedEventId, setErrorMessages, expiryPeriod, + orgUnitLabel, }: RelatedStageIsValidProps) => { if (!linkMode) { return true; @@ -31,5 +32,6 @@ export const relatedStageWidgetIsValid = ({ linkedEventId, setErrorMessages, expiryPeriod, + orgUnitLabel, }); }; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts index c6d401ef37..85ce5d2414 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts @@ -16,4 +16,5 @@ export type RelatedStageIsValidProps = { expiryPeriodType?: string; expiryDays?: number; }; + orgUnitLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts index 668a6e316f..e2feddcbc2 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts @@ -4,8 +4,9 @@ import log from 'loglevel'; import { useDataEngine, useConfig } from '@dhis2/app-runtime'; import { makeQuerySingleResource } from 'capture-core/utils/api'; import { errorCreator, buildUrl } from 'capture-core-utils'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; -import { dataElementTypes, DataElement, OptionSet, Option } from '../../../../../../metaData'; +import { dataElementTypes, DataElement, OptionSet, Option, useTermLabel } from '../../../../../../metaData'; import type { StageDataElement, StageDataElementClient } from '../../../../types/common.types'; import { convertValue as convertClientToList } from '../../../../../../converters/clientToList'; import { convertValue as convertServerToClient } from '../../../../../../converters/serverToClient'; @@ -37,7 +38,7 @@ const getBaseColumnHeaders = props => [ { header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: props.formFoundation.getLabel('occurredAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: i18n.t('Assigned to'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: i18n.t('Organisation unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: props.orgUnitLabel, sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: props.formFoundation.getLabel('scheduledAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: '', sortDirection: null, isPredefined: true }, ]; @@ -118,6 +119,7 @@ const useComputeHeaderColumn = ( enableUserAssignment: boolean, formFoundation?: { getLabel: (key: string) => string }, ) => { + const orgUnitLabel = capitalizeFirstLetter(useTermLabel('orgUnit')); const headerColumns = useMemo(() => { const dataElementHeaders = dataElements.reduce((acc, currDataElement) => { const { id, name, formName, type, optionSet } = currDataElement; @@ -131,13 +133,13 @@ const useComputeHeaderColumn = ( return acc; }, [] as Array<{ id: string; header: string; type: keyof typeof dataElementTypes; sortDirection: string }>); return [ - ...getBaseColumns({ formFoundation }) + ...getBaseColumns({ formFoundation, orgUnitLabel }) .filter(col => (enableUserAssignment || col.id !== 'assignedUser') && (!hideDueDate || col.id !== 'scheduledAt'), ), ...dataElementHeaders]; - }, [dataElements, hideDueDate, enableUserAssignment, formFoundation]); + }, [dataElements, hideDueDate, enableUserAssignment, formFoundation, orgUnitLabel]); return headerColumns; }; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx index 7992e0bcfb..73cece0548 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx @@ -2,7 +2,9 @@ import React, { useMemo } from 'react'; import { spacers } from '@dhis2/ui'; import { FlatList } from 'capture-ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import type { RenderFoundation } from '../../metaData'; +import { useTermLabel } from '../../metaData'; import { getDataEntryDetails, Placements } from './utils/getDataEntryDetails'; type OwnProps = { @@ -23,10 +25,12 @@ const styles: Readonly = { }; const WidgetTwoEventWorkspacePlain = ({ linkedEvent, dataValues, formFoundation, classes }: Props) => { + const orgUnitLabel = capitalizeFirstLetter(useTermLabel('orgUnit')); const dataEntryValues = useMemo(() => getDataEntryDetails( linkedEvent, formFoundation, - ), [linkedEvent, formFoundation]); + orgUnitLabel, + ), [linkedEvent, formFoundation, orgUnitLabel]); const listValues = useMemo(() => { const elements = formFoundation.getElements(); diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts index 7d5c10dbf1..b891ec52db 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts @@ -13,7 +13,7 @@ export const Placements = { BOTTOM: 'BOTTOM', }; -export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: RenderFoundation) => { +export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: RenderFoundation, orgUnitLabel: string) => { const statusLabels: Record = { ACTIVE: i18n.t('Active'), COMPLETED: i18n.t('Completed'), @@ -36,7 +36,7 @@ export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: Re apiKey: 'orgUnit', type: dataElementTypes.ORGANISATION_UNIT, placement: Placements.TOP, - label: i18n.t('Organisation unit'), + label: orgUnitLabel, convertFn: (orgUnitId: string) => React.createElement(TooltipOrgUnit, { orgUnitId }), }, status: { diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts index 0255d9f9f5..2cb62dca58 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts @@ -1,8 +1,9 @@ import { useMemo } from 'react'; import { translatedStatusTypes } from 'capture-core/events/statusTypes'; import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import type { ProgramStage } from '../../../../metaData'; -import { dataElementTypes as elementTypeKeys } from '../../../../metaData'; +import { dataElementTypes as elementTypeKeys, useTermLabel } from '../../../../metaData'; import { mainPropertyNames } from '../../../../events/mainPropertyNames.const'; import type { MainColumnConfig, @@ -11,7 +12,7 @@ import type { } from '..'; -const getDefaultMainConfig = (stage: ProgramStage): Array => { +const getDefaultMainConfig = (stage: ProgramStage, orgUnitLabel: string): Array => { const baseFields = [{ id: mainPropertyNames.OCCURRED_AT, visible: true, @@ -21,7 +22,7 @@ const getDefaultMainConfig = (stage: ProgramStage): Array => { id: mainPropertyNames.ORGANISATION_UNIT, visible: true, type: elementTypeKeys.ORGANISATION_UNIT, - header: i18n.t('Organisation unit'), + header: capitalizeFirstLetter(orgUnitLabel), apiName: 'orgUnit', filterHidden: true, }, { @@ -66,8 +67,10 @@ const getMetaDataConfig = (stage: ProgramStage): Array => multiValueFilter: !!optionSet || type === elementTypeKeys.BOOLEAN, })) as Array; -export const useDefaultColumnConfig = (stage: ProgramStage): EventWorkingListsColumnConfigs => - useMemo(() => [ - ...getDefaultMainConfig(stage), +export const useDefaultColumnConfig = (stage: ProgramStage): EventWorkingListsColumnConfigs => { + const orgUnitLabel = useTermLabel('orgUnit'); + return useMemo(() => [ + ...getDefaultMainConfig(stage, orgUnitLabel), ...getMetaDataConfig(stage), - ], [stage]); + ], [stage, orgUnitLabel]); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts index 21cc49f81c..3b3dcfba83 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts @@ -1,16 +1,17 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { ADDITIONAL_FILTERS, ADDITIONAL_FILTERS_LABELS } from '../../helpers'; -import { dataElementTypes, type TrackerProgram } from '../../../../../metaData'; +import { dataElementTypes, type TrackerProgram, useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import type { MainColumnConfig, MetadataColumnConfig, TrackerWorkingListsColumnConfigs } from '../../types'; -const getMainConfig = (hasDisplayInReportsAttributes: boolean): Array => +const getMainConfig = (hasDisplayInReportsAttributes: boolean, orgUnitLabel: string): Array => [ { id: 'programOwnerId', visible: false, type: dataElementTypes.ORGANISATION_UNIT, - header: i18n.t('Owner organisation unit'), + header: tCustomTerm('Owner {{orgUnitLabel}}', { orgUnitLabel }), sortDisabled: true, filterHidden: true, apiViewName: 'programOwner', @@ -27,7 +28,7 @@ const getMainConfig = (hasDisplayInReportsAttributes: boolean): Array => +const getProgramStageMainConfig = (programStage, orgUnitLabel: string): Array => [ { id: ADDITIONAL_FILTERS.status, @@ -57,7 +58,7 @@ const getProgramStageMainConfig = (programStage): Array => id: ADDITIONAL_FILTERS.orgUnit, visible: true, type: dataElementTypes.ORGANISATION_UNIT, - header: ADDITIONAL_FILTERS_LABELS.orgUnit, + header: tCustomTerm('Event {{orgUnitLabel}}', { orgUnitLabel }), apiViewName: 'eventOrgUnit', }, ...(programStage.enableUserAssignment @@ -141,23 +142,25 @@ export const useDefaultColumnConfig = ( program: TrackerProgram, orgUnitId: string | null | undefined, programStageId: string | null | undefined, -): TrackerWorkingListsColumnConfigs => - useMemo(() => { +): TrackerWorkingListsColumnConfigs => { + const orgUnitLabel = useTermLabel('orgUnit'); + return useMemo(() => { const { attributes, stages } = program; const searchFilterMetaById = buildSearchFilterMetaById(program); const programStage = programStageId && stages.get(programStageId); const hasDisplayInReportsAttributes = attributes.some(attribute => attribute.displayInReports); const defaultColumns = [ - ...getMainConfig(hasDisplayInReportsAttributes), + ...getMainConfig(hasDisplayInReportsAttributes, orgUnitLabel), ...getTEIMetaDataConfig(attributes, orgUnitId, searchFilterMetaById), ]; if (programStageId && programStage) { return defaultColumns.concat([ - ...getProgramStageMainConfig(programStage), + ...getProgramStageMainConfig(programStage, orgUnitLabel), ...getEventsMetaDataConfig(programStage), ]); } return defaultColumns; - }, [orgUnitId, program, programStageId]); + }, [orgUnitId, program, programStageId, orgUnitLabel]); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts index aa7964603b..4023ec950f 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -98,7 +98,7 @@ const resolveTerm = ( }; export const getTermLabel = ( - programId: string | undefined, + programId: string, key: CustomLabelKey, options: TermLabelOptions = {}, ): string => resolveTerm(programId, key, options); diff --git a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx index 0b14108661..4407cec7c6 100644 --- a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx +++ b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { useSelector, useDispatch } from 'react-redux'; import { useOrgUnitGroups } from 'capture-core/hooks/useOrgUnitGroups'; import { useOrganisationUnit } from '../../dataQueries'; import { orgUnitFetched } from './coreOrgUnit.actions'; import type { CoreOrgUnit } from './coreOrgUnit.types'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; export function useCoreOrgUnit(orgUnitId: string): { orgUnit?: CoreOrgUnit, @@ -16,6 +17,13 @@ export function useCoreOrgUnit(orgUnitId: string): { // These hooks do no work when id is undefined const { orgUnit, error } = useOrganisationUnit(fetchId, 'displayName,code,path'); const { orgUnitGroups, error: groupError } = useOrgUnitGroups(fetchId); + const orgUnitLabel = useTermLabel('orgUnit'); + + const errorComponent = ( +
+ {tCustomTerm('{{orgUnitLabel}} could not be retrieved. Please try again later.', { orgUnitLabel })} +
+ ); if (reduxOrgUnit) { return { orgUnit: reduxOrgUnit }; @@ -40,9 +48,3 @@ export function useCoreOrgUnit(orgUnitId: string): { return {}; } - -const errorComponent = ( -
- {i18n.t('organisation unit could not be retrieved. Please try again later.')} -
-); From dbe93b4bee7e353f62c23bc95eefc67724db62b6 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:33:52 +0000 Subject: [PATCH 046/118] feat: update programId handling in enrollment related actions and epics --- i18n/en.pot | 4 +-- .../DataEntryWidgetOutput.container.ts | 3 +-- .../Enrollment/epics/enrollmentPage.epics.ts | 2 +- .../Enrollment/epics/fetchEnrollment.epics.ts | 2 +- .../EnrollmentAddEventPage.epics.ts | 5 ++-- .../EnrollmentEditEventPage.epics.ts | 5 ++-- .../RegistrationDataEntry.actions.ts | 3 +++ .../RegistrationDataEntry.epics.ts | 1 + .../enrollment.actions.ts | 3 ++- .../DataEntry/editEventDataEntry.actions.ts | 2 +- .../editEventDataEntry.actions.ts | 4 +-- .../feedback.reducerDescriptionGetter.ts | 26 ++++++++++--------- 12 files changed, 34 insertions(+), 26 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 283379e212..a987a29ead 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T07:23:02.415Z\n" -"PO-Revision-Date: 2026-09-01T07:23:02.415Z\n" +"POT-Creation-Date: 2026-09-01T07:33:54.478Z\n" +"PO-Revision-Date: 2026-09-01T07:33:54.478Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts b/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts index bff416792a..35ffc08160 100644 --- a/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts +++ b/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts @@ -18,8 +18,7 @@ const makeMapStateToProps = () => { const { dataEntries } = state; const ready = !!dataEntries[dataEntryId]; const dataEntryKey = ready ? getDataEntryKey(dataEntryId, state.dataEntries[dataEntryId].itemId) : null; - const programId = state.currentSelections?.programId; - const enrollmentLabel = getTermLabel(programId, 'enrollment'); + const enrollmentLabel = getTermLabel(selectedScopeId, 'enrollment'); return { ready, diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts b/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts index c606d393bf..d0943bceb1 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts @@ -123,7 +123,7 @@ export const enrollmentIdErrorEpic = (action$: any, store: any) => action$.pipe( ofType(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_ERROR), map(({ payload: { enrollmentId } }) => { - const { programId } = store.value.currentSelections; + const { programId } = store.value.enrollmentPage; const enrollmentLabel = getTermLabel(programId, 'enrollment'); return showErrorViewOnEnrollmentPage({ error: tCustomTerm('{{enrollmentLabel}} with id "{{enrollmentId}}" does not exist', diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts b/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts index c786028e3f..d6c3cc0ed1 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts @@ -128,7 +128,7 @@ const handleErrorsFromNewerBackends = ({ return of(showErrorViewOnEnrollmentPage({ error: errorMessage })); }; -const handleErrorsFromOlderBackends = (error: any, programId?: string) => { +const handleErrorsFromOlderBackends = (error: any, programId: string) => { const { message } = error || {}; if (message) { if (message.includes(serverErrorMessages.OWNERSHIP_ACCESS_PARTIALLY_DENIED)) { diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts index 9fe72e13b7..a8c2c14d63 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts @@ -100,7 +100,7 @@ export const saveNewEventSucceededEpic = (action$: EpicAction, store: Redux }), ); -export const saveNewEventFailedEpic = (action$: EpicAction) => +export const saveNewEventFailedEpic = (action$: EpicAction, store: any) => action$.pipe( ofType( addEnrollmentEventPageDefaultActionTypes.EVENT_SAVE_ERROR, @@ -110,9 +110,10 @@ export const saveNewEventFailedEpic = (action$: EpicAction) => map((action: any) => { const { serverData: { events, enrollments } } = action.meta; const rollbackEvents = events ?? enrollments[0].events; + const { programId } = store.value.enrollmentPage; return batchActions([ - saveFailed(), + saveFailed(programId), rollbackEnrollmentEvents({ events: rollbackEvents, }), diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts index 5d05232f57..1801bd7fae 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts @@ -16,13 +16,14 @@ export const updateEventSucceededEpic = (action$: EpicAction) => return commitEnrollmentEvent(eventId); })); -export const updateEventFailedEpic = (action$: EpicAction) => +export const updateEventFailedEpic = (action$: EpicAction, store: any) => action$.pipe( ofType( editActionTypes.EVENT_SCHEDULE_ERROR, ), map((action: any) => { const { eventId } = action.meta; - return batchActions([saveFailed(), rollbackEnrollmentEvent(eventId)]); + const { programId } = store.value.enrollmentPage; + return batchActions([saveFailed(programId), rollbackEnrollmentEvent(eventId)]); }), ); diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.ts b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.ts index da92b59943..a61f4fb086 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.actions.ts @@ -60,6 +60,7 @@ export const saveNewTrackedEntityInstanceWithEnrollment = ({ candidateForRegistration, uid, redirect, + programId, }: { candidateForRegistration: any; uid: string; @@ -67,6 +68,7 @@ export const saveNewTrackedEntityInstanceWithEnrollment = ({ programStageId?: string; eventId?: string; }; + programId: string; }) => actionCreator(registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE)( { ...candidateForRegistration }, @@ -83,6 +85,7 @@ export const saveNewTrackedEntityInstanceWithEnrollment = ({ }, rollback: { type: registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED, + meta: { programId }, }, }, }, diff --git a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.ts b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.ts index f1d5619fd2..ec7868844d 100644 --- a/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/Pages/New/RegistrationDataEntry/RegistrationDataEntry.epics.ts @@ -57,6 +57,7 @@ export const startSavingNewTrackedEntityInstanceWithEnrollmentEpic = ( }, redirect, uid, + programId: enrollmentPayload.enrollments[0].program, }); }), ); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts index 87300d6fbf..fe2d6bebb4 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts @@ -85,7 +85,8 @@ export const updateEnrollmentEventWithoutId = (uid: string, eventData: any) => uid, }); -export const saveFailed = () => actionCreator(enrollmentSiteActionTypes.SAVE_FAILED)(); +export const saveFailed = (programId: string) => + actionCreator(enrollmentSiteActionTypes.SAVE_FAILED)({ programId }); export const updateEnrollmentAttributeValues = (attributeValues: Array<{ [key: string]: string }>) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_ATTRIBUTE_VALUES)({ diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts index b0948c7904..0ac87af2fe 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts @@ -166,7 +166,7 @@ export const openEventForEditInDataEntry = ({ const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { throw Error(tCustomTerm('{{programStageLabel}} not found in rules execution', { - programStageLabel: getTermLabel(program?.id, 'programStage'), + programStageLabel: getTermLabel(program.id, 'programStage'), })); } // TODO: Add attributeValues & enrollmentData diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts index e88dc48eb8..cadf4b0b8f 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts @@ -67,7 +67,7 @@ export const prerequisitesErrorLoadingEditEventDataEntry = (message: string) => export const requestDeleteEventDataEntry = ({ eventId, enrollmentId }: { eventId: string; enrollmentId: string }) => actionCreator(actionTypes.REQUEST_DELETE_EVENT_DATA_ENTRY)({ eventId, enrollmentId }); -export const startDeleteEventDataEntry = (serverData: any, eventId: string, params: any) => +export const startDeleteEventDataEntry = (serverData: any, eventId: string, params: any, programId: string) => actionCreator(actionTypes.START_DELETE_EVENT_DATA_ENTRY)({ eventId }, { offline: { effect: { @@ -81,7 +81,7 @@ export const startDeleteEventDataEntry = (serverData: any, eventId: string, para }, rollback: { type: actionTypes.DELETE_EVENT_DATA_ENTRY_FAILED, - meta: { eventId, params }, + meta: { eventId, params, programId }, }, }, }); diff --git a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts index 9b3b3e93a0..f9d55cc3eb 100644 --- a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts +++ b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts @@ -111,16 +111,20 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback({ message: i18n.t('Organisation unit search failed.') }), [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error saving tracked entity instance') }), - [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: () => { - const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tCustomTerm('Error saving {{enrollmentLabel}}', { enrollmentLabel }) }); + [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: (_state, action) => { + const enrollmentLabel = getTermLabel(action.meta.programId, 'enrollment'); + return addErrorFeedback({ + message: tCustomTerm('Error saving {{enrollmentLabel}}', { enrollmentLabel }), + }); }, - [enrollmentSiteActionTypes.SAVE_FAILED]: () => { - const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tCustomTerm('Error saving the {{enrollmentLabel}} event', { enrollmentLabel }) }); + [enrollmentSiteActionTypes.SAVE_FAILED]: (_state, action) => { + const enrollmentLabel = getTermLabel(action.payload.programId, 'enrollment'); + return addErrorFeedback({ + message: tCustomTerm('Error saving the {{enrollmentLabel}} event', { enrollmentLabel }), + }); }, - [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: () => { - const enrollmentLabel = getTermLabel(undefined, 'enrollment'); + [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: (_state, action) => { + const enrollmentLabel = getTermLabel(action.meta.programId, 'enrollment'); return addErrorFeedback({ message: tCustomTerm('Error deleting the {{enrollmentLabel}} event', { enrollmentLabel }), }); @@ -133,10 +137,8 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), [enrollmentEditEventActionTypes.ASSIGNEE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), - [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: () => { - const enrollmentLabel = getTermLabel(undefined, 'enrollment'); - return addErrorFeedback({ message: tCustomTerm('Could not save {{enrollmentLabel}} note', { enrollmentLabel }) }); - }, + [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: () => + addErrorFeedback({ message: i18n.t('Could not save enrollment note') }), [eventNoteActionTypes.ADD_NOTE_FAILED_FOR_EVENT]: () => addErrorFeedback({ message: i18n.t('Could not save event note') }), [viewEventNotesActionTypes.SAVE_EVENT_NOTE_FAILED]: () => From d00b1b9a084e7929d0cf90221c8b9c0d34512ffc Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:32:12 +0000 Subject: [PATCH 047/118] fix: update plural label fallback to use singular label for default --- i18n/en.pot | 4 ++-- .../capture-core/metaData/helpers/customLabels.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index b4c8422fb7..510dfd455f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T11:15:51.335Z\n" -"PO-Revision-Date: 2026-08-31T11:15:51.335Z\n" +"POT-Creation-Date: 2026-09-01T10:32:13.670Z\n" +"PO-Revision-Date: 2026-09-01T10:32:13.670Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts index aa7964603b..caf195f551 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -93,7 +93,7 @@ const resolveTerm = ( const stage = program && stageId ? program.getStage(stageId) : undefined; const custom = resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); if (custom) return custom; - if (plural) return LABELS[key].plural?.() ?? `${key}s`; + if (plural) return LABELS[key].plural?.() ?? LABELS[key].singular(); return LABELS[key].singular(); }; From 5f9ea49d2a6aef25693f286d787be6c8d3df0136 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:44:56 +0000 Subject: [PATCH 048/118] feat: event labels singular and plural --- i18n/en.pot | 313 ++---------------- .../EnrollmentBreadcrumb.tsx | 9 +- .../EventBreadcrumb/EventBreadcrumb.tsx | 9 +- .../hooks/useWorkingListLabel.ts | 7 +- ...lmentWithFirstStageDataEntry.component.tsx | 6 +- .../DataEntry/DataEntry.component.tsx | 2 +- .../DataEntry/DataEntry.container.ts | 1 + .../DataEntry/actions/dataEntry.actions.ts | 12 +- .../addRelationshipForNewSingleEvent.epics.ts | 5 +- .../note.validatorContainersGetter.ts | 19 +- .../helpers/getOpenDataEntryActions.ts | 9 +- .../RecentlyAddedEventsList.component.tsx | 17 +- ...wEventNewRelationshipWrapper.component.tsx | 14 +- ...wEventNewRelationshipWrapper.container.tsx | 1 + .../CompleteModal/CompleteModal.component.tsx | 36 +- .../dataEntryOutput/withFeedbackOutput.tsx | 6 +- .../dataEntryOutput/withIndicatorOutput.tsx | 6 +- .../DataEntry/withAskToCreateNew.tsx | 11 +- .../Dialogs/DiscardDialog.constants.ts | 6 +- .../EnrollmentQuickActions.component.tsx | 7 +- ...nrollmentAddEventPageDefault.component.tsx | 8 +- .../WidgetStageHeader.component.tsx | 16 +- .../EnrollmentEditEventPage.component.tsx | 160 ++++----- .../Header/EventWorkingListsInitHeader.tsx | 38 ++- .../StageEventHeader.component.tsx | 55 +-- .../EventDetailsSection.component.tsx | 10 +- .../ViewEvent/Notes/viewEventNotes.actions.ts | 17 +- .../ViewEvent/Notes/viewEventNotes.epics.ts | 2 +- ...wEventNewRelationshipWrapper.component.tsx | 13 +- ...ewEventNewRelationshipWrapper.container.ts | 1 + .../ViewEventNewRelationshipWrapper.types.ts | 1 + .../ViewEventRelationships.epics.ts | 5 +- .../NotesSection/NotesSection.component.tsx | 11 +- .../NotesSection/NotesSection.container.tsx | 5 +- .../NotesSection/NotesSection.types.ts | 3 +- .../RelationshipsSection.component.tsx | 10 +- .../RelationshipsSection.container.tsx | 1 + .../RelationshipsSection.types.ts | 1 + .../ViewEventComponent/ViewEvent.container.ts | 14 +- .../Pages/ViewEvent/epics/editEvent.epics.ts | 10 +- .../LayoutComponentConfig.ts | 3 +- .../SearchOrgUnitSelector.container.ts | 2 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 15 +- .../ReadOnlyBadge/ReadOnlyBadge.types.ts | 1 + .../Relationships/Relationships.component.tsx | 9 +- .../TopBarActions/TopBarActions.component.tsx | 6 +- .../WidgetAssignee/DisplayMode.component.tsx | 52 +-- .../CompleteModal/CompleteModal.component.tsx | 36 +- .../WidgetEnrollment/Date/Date.component.tsx | 6 +- .../DataEntry/DataEntry.component.tsx | 2 +- .../DataEntry/DataEntry.container.tsx | 1 + .../note.validatorContainersGetter.ts | 19 +- .../helpers/getOpenDataEntryActions.ts | 9 +- .../DataEntry/withDeleteButton.tsx | 98 +++--- .../DataEntry/withDeleteButton.types.ts | 1 + .../WidgetHeader/WidgetHeader.container.tsx | 5 +- .../WidgetEventNote.actions.ts | 10 +- .../WidgetEventNote.component.tsx | 14 +- .../WidgetEventNote/WidgetEventNote.epics.ts | 3 +- .../WidgetEventNote/WidgetEventNote.types.ts | 1 + .../InfoBox/InfoBox.component.tsx | 27 +- .../ScheduleDate/ScheduleDate.component.tsx | 10 +- .../ScheduleText/ScheduleText.component.tsx | 28 +- .../WidgetEventSchedule.component.tsx | 9 +- .../LinkToExisting.component.tsx | 9 +- .../RelatedStagesActions.component.tsx | 20 +- .../WidgetRelatedStages.container.tsx | 6 +- .../WidgetRelatedStages/constants.ts | 1 - .../hooks/useAddEventWithRelationship.ts | 8 +- .../StageCreateNewButton.tsx | 18 +- .../DeleteActionButton/DeleteActionButton.tsx | 12 +- .../DeleteActionModal/DeleteActionModal.tsx | 13 +- .../EventRow/SkipAction/SkipAction.tsx | 5 +- .../StageDetail/StageDetail.component.tsx | 11 +- .../StageOverview/StageOverview.component.tsx | 13 +- .../Modal/UnlinkAndDeleteModal.tsx | 19 +- .../OverflowMenu/Modal/UnlinkModal.tsx | 17 +- .../OverflowMenu/OverflowMenu.component.tsx | 21 +- .../WidgetWrapper/WidgetWrapper.container.tsx | 13 +- ...entWorkingListsReduxProvider.container.tsx | 4 +- .../RowMenuSetup/DeleteEventModal.tsx | 11 +- ...ventWorkingListsRowMenuSetup.component.tsx | 14 +- .../epics/eventList.epics.ts | 4 +- .../eventWorkingLists.actions.ts | 6 +- .../Actions/CompleteAction/CompleteAction.tsx | 31 +- .../CompleteAction/CompleteAction.types.ts | 2 +- .../hooks/useBulkCompleteEvents.ts | 6 +- .../Actions/DeleteAction/DeleteAction.tsx | 22 +- .../EventBulkActions/EventBulkActions.tsx | 2 +- .../EventBulkActions.types.ts | 2 +- 90 files changed, 783 insertions(+), 751 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3c9169e71a..7b7d06d9fa 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T11:00:18.429Z\n" -"PO-Revision-Date: 2026-09-01T11:00:18.429Z\n" +"POT-Creation-Date: 2026-09-01T12:44:57.325Z\n" +"PO-Revision-Date: 2026-09-01T12:44:57.325Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -45,21 +45,9 @@ msgstr "Program overview" msgid "Search" msgstr "Search" -msgid "View event" -msgstr "View event" - -msgid "Edit event" -msgstr "Edit event" - -msgid "New event" -msgstr "New event" - msgid "Loading..." msgstr "Loading..." -msgid "Event list" -msgstr "Event list" - msgid "More" msgstr "More" @@ -120,9 +108,6 @@ msgstr "Area" msgid "Coordinate" msgstr "Coordinate" -msgid "Complete event" -msgstr "Complete event" - msgid "" "The date entered belongs to an expired period. Enter a date after " "{{firstValidDate}}." @@ -172,6 +157,9 @@ msgstr "Assigned user" msgid "Search for user" msgstr "Search for user" +msgid "Complete event" +msgstr "Complete event" + msgid "Notes" msgstr "Notes" @@ -200,9 +188,6 @@ msgstr "" "This is not an event program or the metadata is corrupt. See log for " "details." -msgid "This event" -msgstr "This event" - msgid "" "Relationship of type {{relationshipTypeName}} to {{entityName}} already " "exists" @@ -216,9 +201,6 @@ msgstr "Active" msgid "Completed" msgstr "Completed" -msgid "Please add or cancel the note before saving the event" -msgstr "Please add or cancel the note before saving the event" - msgid "Save and add another" msgstr "Save and add another" @@ -246,23 +228,6 @@ msgstr "Switch to form view" msgid "Switch to row view" msgstr "Switch to row view" -msgid "{{count}} event added" -msgid_plural "{{count}} event added" -msgstr[0] "{{count}} event added" -msgstr[1] "{{count}} events added" - -msgid "No events added" -msgstr "No events added" - -msgid "New event relationship" -msgstr "New event relationship" - -msgid "Adding relationship to event." -msgstr "Adding relationship to event." - -msgid "Go back to event without saving relationship" -msgstr "Go back to event without saving relationship" - msgid "Discard unsaved changes?" msgstr "Discard unsaved changes?" @@ -300,11 +265,6 @@ msgstr "An error has occurred. See log for details" msgid "{{programStageName}} completed" msgstr "{{programStageName}} completed" -msgid "{{count}} event in {{programStageName}}" -msgid_plural "{{count}} event in {{programStageName}}" -msgstr[0] "{{count}} event in {{programStageName}}" -msgstr[1] "{{count}} events in {{programStageName}}" - msgid "A duplicate exists (but there were some errors, see log for details" msgstr "A duplicate exists (but there were some errors, see log for details" @@ -340,21 +300,6 @@ msgstr "Form foundation missing. See log for details" msgid "validation failed" msgstr "validation failed" -msgid "No feedback for this event yet" -msgstr "No feedback for this event yet" - -msgid "No indicator output for this event yet" -msgstr "No indicator output for this event yet" - -msgid "Generate new event" -msgstr "Generate new event" - -msgid "Do you want to create another event?" -msgstr "Do you want to create another event?" - -msgid "Yes, create new event" -msgstr "Yes, create new event" - msgid "Back to form" msgstr "Back to form" @@ -376,13 +321,6 @@ msgstr "Some operations are still running. Please wait." msgid "Operations running" msgstr "Operations running" -msgid "" -"This event has unsaved changes. Leaving this page without saving will lose " -"these changes. Are you sure you want to discard unsaved changes?" -msgstr "" -"This event has unsaved changes. Leaving this page without saving will lose " -"these changes. Are you sure you want to discard unsaved changes?" - msgid "No events to display" msgstr "No events to display" @@ -655,9 +593,6 @@ msgstr "Close the notice" msgid "Quick actions" msgstr "Quick actions" -msgid "Schedule an event" -msgstr "Schedule an event" - msgid "Make referral" msgstr "Make referral" @@ -706,9 +641,6 @@ msgstr "Schedule" msgid "Refer" msgstr "Refer" -msgid "New Event" -msgstr "New Event" - msgid "You can't add any more {{ programStageName }} events" msgstr "You can't add any more {{ programStageName }} events" @@ -736,9 +668,6 @@ msgstr "Please select {{category}}." msgid "Search for a {{trackedEntityName}}" msgstr "Search for a {{trackedEntityName}}" -msgid "Registered events" -msgstr "Registered events" - msgid "" "You don't have access to create a {{trackedEntityName}} in the current " "selections" @@ -812,18 +741,9 @@ msgstr "Register" msgid "Back" msgstr "Back" -msgid "events" -msgstr "events" - -msgid "event" -msgstr "event" - msgid "View changelog" msgstr "View changelog" -msgid "Event details" -msgstr "Event details" - msgid "" "Leaving this page will discard any selections you made for a new " "relationship" @@ -834,17 +754,21 @@ msgstr "" msgid "Errors" msgstr "Errors" -msgid "This event doesn't have any notes" -msgstr "This event doesn't have any notes" - -msgid "This event doesn't have any relationships" -msgstr "This event doesn't have any relationships" - msgid "Warnings" msgstr "Warnings" -msgid "Event could not be loaded. Are you sure it exists?" -msgstr "Event could not be loaded. Are you sure it exists?" +msgid "No feedback yet" +msgstr "No feedback yet" + +msgid "No indicator output yet" +msgstr "No indicator output yet" + +msgid "" +"Could not load the requested data. It may not exist or you may not have " +"access." +msgstr "" +"Could not load the requested data. It may not exist or you may not have " +"access." msgid "Event could not be loaded" msgstr "Event could not be loaded" @@ -858,6 +782,9 @@ msgstr "All accessible" msgid "Selected" msgstr "Selected" +msgid "organisation unit" +msgstr "organisation unit" + msgid "Selected program" msgstr "Selected program" @@ -920,12 +847,6 @@ msgstr "You only have view access to this {{trackedEntityName}}" msgid "You only have view access to this tracked entity type" msgstr "You only have view access to this tracked entity type" -msgid "This event is outside the editing period" -msgstr "This event is outside the editing period" - -msgid "This event has been completed" -msgstr "This event has been completed" - msgid "This {{trackedEntityName}} is deactivated" msgstr "This {{trackedEntityName}} is deactivated" @@ -1095,9 +1016,6 @@ msgstr "Create saved list" msgid "Create new in another program..." msgstr "Create new in another program..." -msgid "Create new event" -msgstr "Create new event" - msgid "Search for a {{trackedEntityName}} in {{programName}}" msgstr "Search for a {{trackedEntityName}} in {{programName}}" @@ -1119,9 +1037,6 @@ msgstr "Assigned to" msgid "Edit" msgstr "Edit" -msgid "No one is assigned to this event" -msgstr "No one is assigned to this event" - msgid "Assign" msgstr "Assign" @@ -1158,9 +1073,6 @@ msgstr "Transfer" msgid "An error occurred while transferring ownership" msgstr "An error occurred while transferring ownership" -msgid "Existing dates for auto-generated events will not be updated." -msgstr "Existing dates for auto-generated events will not be updated." - msgid "Latitude" msgstr "Latitude" @@ -1221,18 +1133,6 @@ msgstr "Error" msgid "Warning" msgstr "Warning" -msgid "Delete event" -msgstr "Delete event" - -msgid "Deleting an event is permanent and cannot be undone." -msgstr "Deleting an event is permanent and cannot be undone." - -msgid "Are you sure you want to delete this event? " -msgstr "Are you sure you want to delete this event? " - -msgid "Yes, delete event" -msgstr "Yes, delete event" - msgid "Go to “Schedule” tab to reschedule this event" msgstr "Go to “Schedule” tab to reschedule this event" @@ -1257,12 +1157,6 @@ msgstr "No polygon captured" msgid "Event completed" msgstr "Event completed" -msgid "Notes about this event" -msgstr "Notes about this event" - -msgid "Write a note about this event" -msgstr "Write a note about this event" - msgid "after" msgstr "after" @@ -1280,37 +1174,12 @@ msgid_plural "The scheduled date is {{count}} days {{position}} the suggested da msgstr[0] "The scheduled date is {{count}} day {{position}} the suggested date." msgstr[1] "The scheduled date is {{count}} days {{position}} the suggested date." -msgid "" -"There are {{count}} scheduled event in this program in {{orgUnitName}} on " -"this day." -msgid_plural "" -"There are {{count}} scheduled event in this program in {{orgUnitName}} on " -"this day." -msgstr[0] "" -"There are {{count}} scheduled event in this program in {{orgUnitName}} on " -"this day." -msgstr[1] "" -"There are {{count}} scheduled events in this program in {{orgUnitName}} on " -"this day." - msgid "Schedule date / Due date" msgstr "Schedule date / Due date" -msgid "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" -msgstr "Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}" - -msgid "Scheduling an event in {{stageName}} for {{programName}}" -msgstr "Scheduling an event in {{stageName}} for {{programName}}" - msgid "Schedule info" msgstr "Schedule info" -msgid "Event notes" -msgstr "Event notes" - -msgid "Write a note about this scheduled event" -msgstr "Write a note about this scheduled event" - msgid "Feedback" msgstr "Feedback" @@ -1416,18 +1285,6 @@ msgstr "No attributes configured" msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" -msgid "Choose a {{linkableStageLabel}} event" -msgstr "Choose a {{linkableStageLabel}} event" - -msgid "Select an event" -msgstr "Select an event" - -msgid "{{ linkableStageLabel }} can only have one event" -msgstr "{{ linkableStageLabel }} can only have one event" - -msgid "{{ linkableStageLabel }} has no linkable events" -msgstr "{{ linkableStageLabel }} has no linkable events" - msgid "Actions - {{relationshipName}}" msgstr "Actions - {{relationshipName}}" @@ -1437,21 +1294,9 @@ msgstr "Ambiguous relationships, contact system administrator" msgid "Enter details" msgstr "Enter details" -msgid "Linked event" -msgstr "Linked event" - msgid "Enter details now" msgstr "Enter details now" -msgid "Link to an existing event" -msgstr "Link to an existing event" - -msgid "The event was successfully linked" -msgstr "The event was successfully linked" - -msgid "An error occurred while linking the event" -msgstr "An error occurred while linking the event" - msgid "Scheduled date" msgstr "Scheduled date" @@ -1464,103 +1309,27 @@ msgstr "Please enter a date" msgid "Please select a valid event" msgstr "Please select a valid event" -msgid "New {{ eventName }} event" -msgstr "New {{ eventName }} event" - -msgid "{{occurredAt}} belongs to an expired period. Event cannot be deleted" -msgstr "{{occurredAt}} belongs to an expired period. Event cannot be deleted" - -msgid "This event is outside the edit period" -msgstr "This event is outside the edit period" - -msgid "An error occurred while deleting the event" -msgstr "An error occurred while deleting the event" - -msgid "Are you sure you want to delete this event?" -msgstr "Are you sure you want to delete this event?" - -msgid "An error occurred when updating event status" -msgstr "An error occurred when updating event status" - msgid "Unskip" msgstr "Unskip" msgid "Skip" msgstr "Skip" -msgid "To open this event, please wait until saving is complete" -msgstr "To open this event, please wait until saving is complete" - msgid "Show {{ rest }} more" msgstr "Show {{ rest }} more" msgid "Go to full {{ eventName }}" msgstr "Go to full {{ eventName }}" -msgid "Events could not be retrieved. Please try again later." -msgstr "Events could not be retrieved. Please try again later." - -msgid "{{ count }} event" -msgid_plural "{{ count }} event" -msgstr[0] "{{ count }} event" -msgstr[1] "{{count}} events" - msgid "{{ overdueEvents }} overdue" msgstr "{{ overdueEvents }} overdue" msgid "{{ scheduledEvents }} scheduled" msgstr "{{ scheduledEvents }} scheduled" -msgid "An error occurred while unlinking and deleting the event." -msgstr "An error occurred while unlinking and deleting the event." - -msgid "Unlink and delete linked event" -msgstr "Unlink and delete linked event" - -msgid "Are you sure you want to remove the link and delete the linked event?" -msgstr "Are you sure you want to remove the link and delete the linked event?" - -msgid "" -"This action permanently removes the link, linked event, and all related " -"data." -msgstr "" -"This action permanently removes the link, linked event, and all related " -"data." - -msgid "Yes, unlink and delete linked event" -msgstr "Yes, unlink and delete linked event" - -msgid "Unlink event" -msgstr "Unlink event" - -msgid "Are you sure you want to remove the link between these events?" -msgstr "Are you sure you want to remove the link between these events?" - -msgid "This action removes the link itself, but the linked event will remain." -msgstr "This action removes the link itself, but the linked event will remain." - -msgid "Yes, unlink event" -msgstr "Yes, unlink event" - -msgid "View linked event" -msgstr "View linked event" - -msgid "You do not have access to remove the link between these events" -msgstr "You do not have access to remove the link between these events" - -msgid "You do not have access to remove the link and delete the linked event" -msgstr "You do not have access to remove the link and delete the linked event" - msgid "An error occurred while loading the widget." msgstr "An error occurred while loading the widget." -msgid "" -"This {{stageName}} event is linked to a {{linkedStageName}} event. Review " -"the linked event details before entering data below" -msgstr "" -"This {{stageName}} event is linked to a {{linkedStageName}} event. Review " -"the linked event details before entering data below" - msgid "Scheduled" msgstr "Scheduled" @@ -1663,51 +1432,18 @@ msgstr "Download data..." msgid "an error occurred loading working lists" msgstr "an error occurred loading working lists" -msgid "You do not have access to complete events" -msgstr "You do not have access to complete events" - msgid "There is a bulk data entry with unsaved changes" msgstr "There is a bulk data entry with unsaved changes" -msgid "Complete events" -msgstr "Complete events" - -msgid "Are you sure you want to complete all active events in selection?" -msgstr "Are you sure you want to complete all active events in selection?" - -msgid "There are no active events to complete in the current selection." -msgstr "There are no active events to complete in the current selection." - -msgid "Error completing events" -msgstr "Error completing events" - -msgid "There was an error completing the events." -msgstr "There was an error completing the events." - msgid "Details (Advanced)" msgstr "Details (Advanced)" msgid "An unknown error occurred." msgstr "An unknown error occurred." -msgid "An error occurred while completing events" -msgstr "An error occurred while completing events" - -msgid "You do not have access to delete events" -msgstr "You do not have access to delete events" - -msgid "An error occurred while deleting the events" -msgstr "An error occurred while deleting the events" - -msgid "Delete events" -msgstr "Delete events" - msgid "This cannot be undone." msgstr "This cannot be undone." -msgid "Are you sure you want to delete the selected events?" -msgstr "Are you sure you want to delete the selected events?" - msgid "Registration Date" msgstr "Registration Date" @@ -1833,6 +1569,12 @@ msgstr "enrollment" msgid "enrollments" msgstr "enrollments" +msgid "event" +msgstr "event" + +msgid "events" +msgstr "events" + msgid "program stage" msgstr "program stage" @@ -1848,9 +1590,6 @@ msgstr "relationship" msgid "attribute" msgstr "attribute" -msgid "organisation unit" -msgstr "organisation unit" - msgid "follow-up" msgstr "follow-up" diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx index 633bbb4f6e..e275a0085a 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useMemo, useState, ComponentType } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { colors } from '@dhis2/ui'; import { useTermLabel } from '../../../metaData'; @@ -71,6 +70,7 @@ const BreadcrumbsPlain = ({ }: Props) => { const [openWarning, setOpenWarning] = useState(null); const enrollmentLabel = useTermLabel('enrollment', { programId }); + const eventLabel = useTermLabel('event', { programId }); const { label } = useWorkingListLabel({ programId, @@ -111,7 +111,7 @@ const BreadcrumbsPlain = ({ { key: pageKeys.VIEW_EVENT, onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT), - label: i18n.t('View event'), + label: tCustomTerm('View {{eventLabel}}', { eventLabel }), selected: page === pageKeys.VIEW_EVENT, condition: page === pageKeys.VIEW_EVENT || (page === pageKeys.EDIT_EVENT && !eventIsScheduled(eventStatus)), @@ -119,14 +119,14 @@ const BreadcrumbsPlain = ({ { key: pageKeys.EDIT_EVENT, onClick: () => undefined, - label: i18n.t('Edit event'), + label: tCustomTerm('Edit {{eventLabel}}', { eventLabel }), selected: page === pageKeys.EDIT_EVENT, condition: page === pageKeys.EDIT_EVENT, }, { key: pageKeys.NEW_EVENT, onClick: () => undefined, - label: i18n.t('New event'), + label: tCustomTerm('New {{eventLabel}}', { eventLabel }), selected: page === pageKeys.NEW_EVENT, condition: page === pageKeys.NEW_EVENT, }, @@ -139,6 +139,7 @@ const BreadcrumbsPlain = ({ onBackToDashboard, onBackToViewEvent, enrollmentLabel, + eventLabel, ]); return ( diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx index b19159a950..949bcd4d51 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx @@ -1,5 +1,4 @@ import React, { ComponentType, useCallback, useMemo, useState } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { colors } from '@dhis2/ui'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { DirectionalChevron } from '../../../utils/rtl'; @@ -7,6 +6,8 @@ import { BreadcrumbItem } from '../common/BreadcrumbItem'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const pageKeys = { MAIN_PAGE: 'mainPage', @@ -43,6 +44,7 @@ const EventBreadcrumbPlain = ({ }: Props) => { const [openWarning, setOpenWarning] = useState(null); const { label } = useWorkingListLabel({ programId }); + const eventLabel = useTermLabel('event', { programId }); const handleNavigation = useCallback((callback?: () => void, warningType?: PageKeys) => { if (userInteractionInProgress && warningType) { @@ -71,14 +73,14 @@ const EventBreadcrumbPlain = ({ { key: pageKeys.VIEW_EVENT, onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT), - label: i18n.t('View event'), + label: tCustomTerm('View {{eventLabel}}', { eventLabel }), selected: page === pageKeys.VIEW_EVENT, condition: page === pageKeys.VIEW_EVENT || page === pageKeys.EDIT_EVENT, }, { key: pageKeys.EDIT_EVENT, onClick: () => undefined, - label: i18n.t('Edit event'), + label: tCustomTerm('Edit {{eventLabel}}', { eventLabel }), selected: page === pageKeys.EDIT_EVENT, condition: page === pageKeys.EDIT_EVENT, }, @@ -88,6 +90,7 @@ const EventBreadcrumbPlain = ({ onBackToViewEvent, onBackToMainPage, page, + eventLabel, ]); return ( diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts index 5191a000c9..72722ccaa5 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts @@ -1,6 +1,8 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useSelector } from 'react-redux'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; type Template = { id: string; @@ -15,6 +17,7 @@ type Props = { export const useWorkingListLabel = ({ programId }: Props) => { const workingListTemplate = useSelector((state: any) => state.workingListsTemplates?.eventList); const workingListProgramId = useSelector((state: any) => state.workingListsContext?.eventList?.programIdView); + const eventLabel = useTermLabel('event', { programId }); const { selectedTemplateId, @@ -33,8 +36,8 @@ export const useWorkingListLabel = ({ programId }: Props) => { return selectedTemplete.name; } - return i18n.t('Event list'); - }, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete]); + return tCustomTerm('{{eventLabel}} list', { eventLabel }); + }, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete, eventLabel]); return { label: computedLabel, diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx index 620d33dd1d..fabbf82396 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx @@ -1,5 +1,7 @@ import i18n from '@dhis2/d2-i18n'; import { isLangRtl } from '../../../../utils/rtl'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { DataEntry } from '../../../DataEntry'; import { Assignee } from '../../SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/Assignee'; import { @@ -177,7 +179,9 @@ const getCompleteFieldSettingsFn = () => { isApplicable: (props: any) => props.firstStageMetaData && props.firstStageMetaData.stage?.stageForm, getComponent: () => completeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Complete event'), + label: tCustomTerm('Complete {{eventLabel}}', { + eventLabel: getTermLabel(props.programId, 'event'), + }), id: 'complete', }), getPropName: () => stageMainDataIds.COMPLETE, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index 6fb70b2c05..806d92803e 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -408,7 +408,7 @@ const buildNotesSettingsFn = () => { dataEntryId: props.id, }), getPropName: () => 'note', - getValidatorContainers: () => getNoteValidatorContainers(), + getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel), getMeta: () => ({ placement: placements.BOTTOM, section: dataEntrySectionNames.NOTES, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts index b9362b0201..5e49de8368 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts @@ -29,6 +29,7 @@ import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; const customLabels = { orgUnitLabel: { key: 'orgUnit' }, + eventLabel: { key: 'event' }, } as const; const makeMapStateToProps = () => { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts index bd76dee431..3838a93553 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts @@ -58,6 +58,7 @@ export const newEventSavedAfterReturnedToMainPage = (selections: any) => export const startSaveNewEventAfterReturnedToMainPage = (serverData: any, relationshipData: any, selections: any) => { const actionType = actionTypes.START_SAVE_AFTER_RETURNED_TO_MAIN_PAGE; + const programId = serverData?.events?.[0]?.program; return actionCreator(actionType)({ selections }, { offline: { effect: { @@ -69,7 +70,10 @@ export const startSaveNewEventAfterReturnedToMainPage = (serverData: any, relati type: actionTypes.SAVE_NEW_EVENT_RELATIONSHIPS_IF_EXISTS, meta: { selections, relationshipData, triggerAction: actionType }, }, - rollback: { type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_AFTER_RETURNED_TO_MAIN_PAGE, meta: { selections } }, + rollback: { + type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_AFTER_RETURNED_TO_MAIN_PAGE, + meta: { selections, programId }, + }, }, }); }; @@ -156,6 +160,7 @@ export const startSaveNewEventAddAnother = clientId: string, ) => { const actionType = actionTypes.START_SAVE_NEW_EVENT_ADD_ANOTHER; + const programId = serverData?.events?.[0]?.program; return actionCreator(actionTypes.START_SAVE_NEW_EVENT_ADD_ANOTHER)({ selections }, { offline: { effect: { @@ -168,7 +173,10 @@ export const startSaveNewEventAddAnother = type: actionTypes.SAVE_NEW_EVENT_RELATIONSHIPS_IF_EXISTS, meta: { selections, relationshipData, triggerAction: actionType }, }, - rollback: { type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_ADD_ANOTHER, meta: { selections, clientId } }, + rollback: { + type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_ADD_ANOTHER, + meta: { selections, clientId, programId }, + }, }, }); }; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts index 3d71ca94aa..c976ceedd3 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts @@ -4,6 +4,8 @@ import { map } from 'rxjs/operators'; import i18n from '@dhis2/d2-i18n'; import { batchActions } from 'redux-batched-actions'; import type { EpicAction, ReduxStore } from 'capture-core-utils/types'; +import { getTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; import { initializeNewRelationship, @@ -76,11 +78,12 @@ export const addRelationshipForNewSingleEventEpic = (action$: EpicAction !value; -export const getNoteValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateNote, - errorMessage: i18n.t('Please add or cancel the note before saving the event'), - }, - ]; - return validatorContainers; -}; +export const getNoteValidatorContainers = (eventLabel: string) => [ + { + validator: validateNote, + errorMessage: tCustomTerm('Please add or cancel the note before saving the {{eventLabel}}', { + eventLabel, + }), + }, +]; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts index 910720a372..e5f95aef47 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts @@ -10,7 +10,7 @@ import type { ProgramCategory } from '../../../../../WidgetEventSchedule/Categor import type { DataEntryPropToInclude } from '../../../../../DataEntry/actions/dataEntryLoad.utils'; import { getTermLabel } from '../../../../../../metaData/helpers/customLabels'; -const buildDataEntryPropsToInclude = (orgUnitLabel: string): Array => [ +const buildDataEntryPropsToInclude = (orgUnitLabel: string, eventLabel: string): Array => [ { id: 'occurredAt', type: 'DATE', @@ -29,7 +29,7 @@ const buildDataEntryPropsToInclude = (orgUnitLabel: string): Array ({ id: `attributeCategoryOptions-${category.id}`, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx index 06697c00e2..a2f1b8a33b 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Card } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { OfflineEventsList } from '../../../../EventsList/OfflineEventsList/OfflineEventsList.component'; import { listId } from './RecentlyAddedEventsList.const'; import type { Props } from './RecentlyAddedEventsList.types'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const styles = (theme: any) => ({ container: { @@ -18,6 +19,8 @@ const styles = (theme: any) => ({ const NewEventsListPlain = (props: Props & WithStyles) => { const { classes, ...passOnProps } = props; + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); const eventsAdded = props.events ? Object.keys(props.events).length : 0; if (eventsAdded === 0) { return null; @@ -27,16 +30,18 @@ const NewEventsListPlain = (props: Props & WithStyles) => {
- {i18n.t('{{count}} event added', { + {tCustomTerm('{{count}} {{eventLabel}} added', { count: eventsAdded, - defaultValue: '{{count}} event added', - defaultValue_plural: '{{count}} events added', + eventLabel, + eventsLabel, + defaultValue: '{{count}} {{eventLabel}} added', + defaultValue_plural: '{{count}} {{eventsLabel}} added', })}
diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx index 37c83bfbfe..153b2e0cbd 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx @@ -5,6 +5,8 @@ import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { NewRelationship } from '../../../Pages/NewRelationship/NewRelationship.container'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const getStyles = (theme: any) => ({ headerContainer: { @@ -43,6 +45,7 @@ type Props = { onCancel: (dataEntryid: string) => void; dataEntryKey: string; unsavedRelationships: any; + programId: string; }; type State = { @@ -84,27 +87,28 @@ class NewEventNewRelationshipWrapper extends React.Component
- {i18n.t('New event relationship')} + {tCustomTerm('New {{eventLabel}} relationship', { eventLabel: getTermLabel(this.props.programId, 'event') })}
); render() { - const { classes, onCancel, ...passOnProps } = this.props; + const { classes, onCancel, programId, ...passOnProps } = this.props; + const eventLabel = getTermLabel(programId, 'event'); return (
- {i18n.t('Adding relationship to event.')} + {tCustomTerm('Adding relationship to {{eventLabel}}.', { eventLabel })} - {i18n.t('Go back to event without saving relationship')} + {tCustomTerm('Go back to {{eventLabel}} without saving relationship', { eventLabel })}
{ return { relationshipTypes, unsavedRelationships, + programId: state.currentSelections.programId, }; }; diff --git a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx index 52a8829372..39fe1af5b7 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx @@ -14,6 +14,8 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ onCompleteEnrollment, }: PlainPropsWithEvents) => { const enrollmentLabel = useTermLabel('enrollment'); + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); return ( @@ -24,23 +26,24 @@ export const CompleteEnrollmentAndEventsModalComponent = ({

{tCustomTerm( - 'Would you like to complete the {{enrollmentLabel}} and all active events as well?', - { enrollmentLabel }, + 'Would you like to complete the {{enrollmentLabel}} and all active {{eventsLabel}} as well?', + { enrollmentLabel, eventsLabel }, )}

{Object.keys(programStagesWithActiveEvents).length !== 0 && ( <> - {i18n.t('The following events will be completed:')} + {tCustomTerm('The following {{eventsLabel}} will be completed:', { eventsLabel })} {Object.keys(programStagesWithActiveEvents).map((key) => { const { count, name } = programStagesWithActiveEvents[key]; return (
    - {i18n.t('{{count}} event in {{programStageName}}', { + {tCustomTerm('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel, + eventsLabel, programStageName: name, - interpolation: { escapeValue: false }, + defaultValue: '{{count}} {{eventLabel}} in {{programStageName}}', + defaultValue_plural: '{{count}} {{eventsLabel}} in {{programStageName}}', })}
); @@ -50,18 +53,22 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ {Object.keys(programStagesWithoutAccess).length !== 0 && ( <> - {i18n.t('The following events will not be completed due to lack of access:')} + {tCustomTerm( + 'The following {{eventsLabel}} will not be completed due to lack of access:', + { eventsLabel }, + )} {Object.keys(programStagesWithoutAccess).map((key) => { const { count, name } = programStagesWithoutAccess[key]; return (
    - {i18n.t('{{count}} event in {{programStageName}}', { + {tCustomTerm('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel, + eventsLabel, programStageName: name, - interpolation: { escapeValue: false }, + defaultValue: '{{count}} {{eventLabel}} in {{programStageName}}', + defaultValue_plural: '{{count}} {{eventsLabel}} in {{programStageName}}', })}
); @@ -72,7 +79,10 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ diff --git a/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts b/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts index d7e6cb3f24..56483f54ed 100644 --- a/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts +++ b/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts @@ -2,8 +2,10 @@ import i18n from '@dhis2/d2-i18n'; export const defaultDialogProps = { header: i18n.t('Discard unsaved changes?'), - // eslint-disable-next-line max-len - text: i18n.t('This event has unsaved changes. Leaving this page without saving will lose these changes. Are you sure you want to discard unsaved changes?'), + text: i18n.t( + 'This form has unsaved changes. Leaving this page without saving will lose these changes. ' + + 'Are you sure you want to discard unsaved changes?', + ), destructiveText: i18n.t('Yes, discard changes'), cancelText: i18n.t('No, cancel'), }; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx index 6bb4b27e7d..0325b2b2ca 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx @@ -9,6 +9,8 @@ import { tabMode } from '../../../EnrollmentAddEvent/NewEventWorkspace/newEventW import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../../../utils/routing'; import { useEnrollmentAccessContext } from '../../../common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { OwnProps, ProgramStage, EventCount } from './EnrollmentQuickActions.types'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const styles = { contentContainer: { @@ -30,6 +32,7 @@ const EnrollmentQuickActionsComponentPlain = ({ const { navigate } = useNavigate(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { anyStageWriteAccess } = useEnrollmentAccessContext(); + const eventLabel = useTermLabel('event', { programId: programId as string | undefined }); const stagesWithEventCount = useMemo(() => stages.map((stage) => { const mutatedStage = { ...stage }; @@ -79,7 +82,7 @@ const EnrollmentQuickActionsComponentPlain = ({ > } - label={i18n.t('New event')} + label={tCustomTerm('New {{eventLabel}}', { eventLabel })} onClickAction={() => onNavigationFromQuickActions(tabMode.REPORT)} dataTest={'quick-action-button-report'} disabled={noStageAvailable} @@ -87,7 +90,7 @@ const EnrollmentQuickActionsComponentPlain = ({ } - label={i18n.t('Schedule an event')} + label={tCustomTerm('Schedule an {{eventLabel}}', { eventLabel })} onClickAction={() => onNavigationFromQuickActions(tabMode.SCHEDULE)} dataTest={'quick-action-button-schedule'} disabled={noStageAvailable} diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx index b09e5879f0..bd29d55fae 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx @@ -8,7 +8,8 @@ import { EnrollmentAccessProvider } from '../../common/EnrollmentOverviewDomain' import { EnrollmentPageKeys, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; -import { TrackerProgram } from '../../../../metaData'; +import { TrackerProgram, useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const styles: Readonly = ({ typography }: any) => ({ container: { @@ -58,6 +59,7 @@ const EnrollmentAddEventPagePain = ({ classes, ...passOnProps }: Props & WithStyles) => { + const eventLabel = useTermLabel('event', { programId: program?.id }); if (pageFailure) { return (
@@ -93,8 +95,8 @@ const EnrollmentAddEventPagePain = ({ onEnrollmentError={onEnrollmentError} onEnrollmentSuccess={onEnrollmentSuccess} onAccessLostFromTransfer={onAccessLostFromTransfer} - feedbackEmptyText={i18n.t('No feedback for this event yet')} - indicatorEmptyText={i18n.t('No indicator output for this event yet')} + feedbackEmptyText={tCustomTerm('No feedback for this {{eventLabel}} yet', { eventLabel })} + indicatorEmptyText={tCustomTerm('No indicator output for this {{eventLabel}} yet', { eventLabel })} />
diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/WidgetStageHeader/WidgetStageHeader.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/WidgetStageHeader/WidgetStageHeader.component.tsx index e61c01df3e..e6f1a320b6 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/WidgetStageHeader/WidgetStageHeader.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/WidgetStageHeader/WidgetStageHeader.component.tsx @@ -1,9 +1,13 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import type { Props } from './widgetStageHeader.types'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; -export const WidgetStageHeader = ({ stage }: Props) => ( -
- {stage?.stageForm.name ?? i18n.t('New Event')} -
-); +export const WidgetStageHeader = ({ stage }: Props) => { + const eventLabel = useTermLabel('event'); + return ( +
+ {stage?.stageForm.name ?? tCustomTerm('New {{eventLabel}}', { eventLabel })} +
+ ); +}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx index b5a96db2d1..e6af1b8a8f 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { dataEntryIds } from 'capture-core/constants'; import type { PlainProps } from './EnrollmentEditEventPage.types'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; import { NoticeBox } from '../../NoticeBox'; @@ -62,80 +63,83 @@ export const EnrollmentEditEventPageComponent = ({ onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, userInteractionInProgress, -}: PlainProps) => ( - - - - - -); +}: PlainProps) => { + const eventLabel = useTermLabel('event', { programId: program?.id }); + return ( + + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WorkingListsType/EventWorkingListsInit/Header/EventWorkingListsInitHeader.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WorkingListsType/EventWorkingListsInit/Header/EventWorkingListsInitHeader.tsx index 538a2fe4a5..f9fce71afc 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WorkingListsType/EventWorkingListsInit/Header/EventWorkingListsInitHeader.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WorkingListsType/EventWorkingListsInit/Header/EventWorkingListsInitHeader.tsx @@ -1,8 +1,9 @@ import { colors, spacers } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import React, { type ComponentType } from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { type Props } from './eventWorkingListsInitHeader.types'; +import { useTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; export const styles = () => ({ container: { @@ -25,24 +26,27 @@ export const styles = () => ({ }); const EventWorkingListsInitHeaderPlain = - ({ children, classes: { container, headerContainer, listContainer, title } }: Props & WithStyles) => ( -
-
- ) => { + const eventsLabel = useTermLabel('event', { plural: true }); + return ( +
+
- {i18n.t('Registered events')} - -
-
- {children} + + {tCustomTerm('Registered {{eventsLabel}}', { eventsLabel })} + +
+
+ {children} +
-
- ); + ); + }; export const EventWorkingListsInitHeader = withStyles(styles)(EventWorkingListsInitHeaderPlain) as ComponentType; diff --git a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx index 2fd01b3acb..e2f6034354 100644 --- a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx +++ b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx @@ -1,9 +1,10 @@ import React, { type ComponentType } from 'react'; import { colors, spacersNum } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { NonBundledDhis2Icon } from '../../../../NonBundledDhis2Icon'; import type { PlainProps } from './StageEventHeader.types'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const getStyles = () => ({ wrapper: { @@ -21,28 +22,38 @@ const getStyles = () => ({ type Props = PlainProps & WithStyles; -const StageEventHeaderPlain = ({ icon, title, events, classes }: Props) => (<> -
-
{ - icon && ( -
- -
- ) - }
-
{title} - {events.length > 0 && : - {events.length} {events.length > 1 ? i18n.t('events') : i18n.t('event')} - } +const StageEventHeaderPlain = ({ icon, title, events, classes }: Props) => { + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); + return (<> +
+
{ + icon && ( +
+ +
+ ) + }
+
{title} + {events.length > 0 && : + {tCustomTerm('{{count}} {{eventLabel}}', { + count: events.length, + eventLabel, + eventsLabel, + defaultValue: '{{count}} {{eventLabel}}', + defaultValue_plural: '{{count}} {{eventsLabel}}', + })} + } +
-
-); + ); +}; export const StageEventHeader = withStyles( getStyles, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.tsx index c33d38c51f..f57e402fbb 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/EventDetailsSection/EventDetailsSection.component.tsx @@ -27,6 +27,8 @@ import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramS import { useProgramExpiryForUser } from '../../../../hooks'; import { useAuthorities } from '../../../../utils/authority/useAuthorities'; import type { PlainProps } from './EventDetailsSection.types'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const getStyles: any = () => ({ container: { @@ -82,6 +84,7 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => { const [actionsIsOpen, setActionsIsOpen] = useState(false); const expiryPeriod = useProgramExpiryForUser(programId); const { hasAuthority: canUncompleteEvent } = useAuthorities({ authorities: ['F_UNCOMPLETE_EVENT'] }); + const eventLabel = useTermLabel('event', { programId }); const onSaveExternal = useCallback(() => { const queryKey = [ReactQueryAppNamespace, 'changelog', CHANGELOG_ENTITY_TYPES.EVENT, eventId]; @@ -126,7 +129,7 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => { secondary small > - {i18n.t('Edit event')} + {tCustomTerm('Edit {{eventLabel}}', { eventLabel })}
} { - + {renderActionsContainer()}
)} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts index c7c157aea8..512dac10fa 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts @@ -22,10 +22,16 @@ export const eventNotesLoaded = () => export const updateEventNoteField = (value: string) => actionCreator(actionTypes.UPDATE_EVENT_NOTE_FIELD)({ value }); -export const requestSaveEventNote = (note: string) => - actionCreator(actionTypes.REQUEST_SAVE_EVENT_NOTE)({ note }); +export const requestSaveEventNote = (note: string, programId: string) => + actionCreator(actionTypes.REQUEST_SAVE_EVENT_NOTE)({ note, programId }); -export const startSaveEventNote = (eventUid: string, serverData: any, selections: any, clientId: string) => +export const startSaveEventNote = ( + eventUid: string, + serverData: any, + selections: any, + clientId: string, + programId: string, +) => actionCreator(actionTypes.START_SAVE_EVENT_NOTE)({ selections, clientId }, { offline: { effect: { @@ -36,6 +42,9 @@ export const startSaveEventNote = (eventUid: string, serverData: any, selections data: serverData, }, commit: { type: actionTypes.EVENT_NOTE_SAVED, meta: { selections, clientId } }, - rollback: { type: actionTypes.SAVE_EVENT_NOTE_FAILED, meta: { selections, clientId } }, + rollback: { + type: actionTypes.SAVE_EVENT_NOTE_FAILED, + meta: { selections, clientId, programId }, + }, }, }); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts index 65bc6bb88b..2357dc02ca 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts @@ -69,7 +69,7 @@ export const addNoteForViewEventEpic = (action$: any, store: any, { fromClientDa clientId, }; return batchActions([ - startSaveEventNote(eventId, serverData, state.currentSelections, clientNote.clientId), + startSaveEventNote(eventId, serverData, state.currentSelections, clientNote.clientId, payload.programId), addNote(noteKey, clientNote), ], viewEventNotesBatchActionTypes.SAVE_EVENT_NOTE_BATCH); })); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx index f8ccbe51e4..c46faecc13 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx @@ -7,6 +7,8 @@ import { NewRelationship } from '../../NewRelationship/NewRelationship.container import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { LinkButton } from '../../../Buttons/LinkButton.component'; import type { PlainProps } from './ViewEventNewRelationshipWrapper.types'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const getStyles = (theme: any) => ({ container: { @@ -70,27 +72,28 @@ class ViewEventNewRelationshipWrapperPlain extends React.Component className={this.props.classes.headerContainer} >
- {i18n.t('New event relationship')} + {tCustomTerm('New {{eventLabel}} relationship', { eventLabel: getTermLabel(this.props.programId, 'event') })}
); render() { - const { classes, onCancel, ...passOnProps } = this.props; + const { classes, onCancel, programId, ...passOnProps } = this.props; + const eventLabel = getTermLabel(programId, 'event'); return (
- {i18n.t('Adding relationship to event.')} + {tCustomTerm('Adding relationship to {{eventLabel}}.', { eventLabel })} - {i18n.t('Go back to event without saving relationship')} + {tCustomTerm('Go back to {{eventLabel}} without saving relationship', { eventLabel })}
diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.container.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.container.ts index 70d3e81f54..50c5d2d30c 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.container.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.container.ts @@ -11,6 +11,7 @@ const makeMapStateToProps = () => { return { relationshipTypes, + programId: state.currentSelections.programId, }; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts index af8b72fc1c..7b45ac219b 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts @@ -1,3 +1,4 @@ export type PlainProps = { onCancel: () => void; + programId: string; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts index 845d480e4a..94d3744aa4 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts @@ -3,6 +3,8 @@ import { ofType } from 'redux-observable'; import { map, switchMap } from 'rxjs/operators'; import i18n from '@dhis2/d2-i18n'; import uuid from 'd2-utilizr/lib/uuid'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { addRelationship, removeRelationship, @@ -79,11 +81,12 @@ export const addRelationshipForViewEventEpic = (action$: any, store: any) => const toEntity = payload.entity; const relationshipClientId = uuid(); + const programId = state.currentSelections.programId; const clientRelationship = { clientId: relationshipClientId, from: { id: eventId, - name: i18n.t('This event'), + name: tCustomTerm('This {{eventLabel}}', { eventLabel: getTermLabel(programId, 'event') }), type: 'PROGRAM_STAGE_INSTANCE', }, to: { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx index aba080dd1b..f9e54b2242 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx @@ -9,6 +9,8 @@ import { ViewEventSectionHeader } from '../../Section/ViewEventSectionHeader.com import { Notes } from '../../../../Notes/Notes.component'; import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; import type { PlainProps } from './NotesSection.types'; +import { getTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const LoadingNotes = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Notes); @@ -48,7 +50,7 @@ class NotesSectionPlain extends React.Component { } render() { - const { classes, notes, fieldValue, onAddNote, ready, readOnly } = this.props; + const { classes, notes, fieldValue, onAddNote, ready, readOnly, programId } = this.props; const isEmpty = ready && (!notes || notes.length === 0); return ( { > {isEmpty && (
- {i18n.t("This event doesn't have any notes")} + {tCustomTerm( + "This {{eventLabel}} doesn't have any notes", + { eventLabel: getTermLabel(programId, 'event') }, + )}
)} {React.createElement(LoadingNotes as any, { ready, notes, readOnly, - onAddNote, + onAddNote: (note: string) => onAddNote(note, programId), onBlur: this.props.onUpdateNoteField, value: fieldValue, smallMainButton: true, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx index b783a89832..17eb4bfc99 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx @@ -8,12 +8,13 @@ const mapStateToProps = (state: any) => { notes: state.notes.viewEvent || [], ready: !notesSection.isLoading, fieldValue: notesSection.fieldValue, + programId: state.currentSelections.programId, }; }; const mapDispatchToProps = (dispatch: any) => ({ - onAddNote: (note: string) => { - dispatch(requestSaveEventNote(note)); + onAddNote: (note: string, programId: string) => { + dispatch(requestSaveEventNote(note, programId)); }, onUpdateNoteField: (value: string) => { dispatch(updateEventNoteField(value)); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts index 2a4bc4e5e4..9f9e2add8d 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts @@ -1,8 +1,9 @@ export type PlainProps = { notes?: Array; - onAddNote: (note: string) => void; + onAddNote: (note: string, programId: string) => void; onUpdateNoteField: (value: string) => void; fieldValue?: string; ready: boolean; readOnly: boolean; + programId: string; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx index b9f3302044..c98b310f95 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx @@ -11,6 +11,8 @@ import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; import { ConnectedEntity } from './ConnectedEntity'; import type { Entity } from '../../../../Relationships/relationships.types'; import type { PlainProps } from './RelationshipsSection.types'; +import { getTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const LoadingRelationships = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Relationships); @@ -76,7 +78,7 @@ class RelationshipsSectionPlain extends React.Component { } render() { - const { classes, programStage, eventId, relationships, ready, readOnly } = this.props; + const { classes, programStage, programId, eventId, relationships, ready, readOnly } = this.props; const relationshipTypes = programStage.relationshipTypes || []; const hasRelationshipTypes = relationshipTypes.length > 0; @@ -92,7 +94,10 @@ class RelationshipsSectionPlain extends React.Component { > {isEmpty && (
- {i18n.t("This event doesn't have any relationships")} + {tCustomTerm( + "This {{eventLabel}} doesn't have any relationships", + { eventLabel: getTermLabel(programId, 'event') }, + )}
)} {React.createElement(LoadingRelationships as any, { @@ -105,6 +110,7 @@ class RelationshipsSectionPlain extends React.Component { readOnly, smallMainButton: true, onRenderConnectedEntity: this.renderConnectedEntity, + programId, })}
); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx index 63e144aeaa..3b8baaba90 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx @@ -7,6 +7,7 @@ const mapStateToProps = (state: any) => { const relationshipsSection = state.viewEventPage.relationshipsSection || {}; return { eventId: state.viewEventPage.eventId, + programId: state.currentSelections.programId, ready: !relationshipsSection.isLoading, relationships: state.relationships.viewEvent || [], orgUnitId: state.currentSelections.orgUnitId, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts index 005b364885..601a1bfbb6 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts @@ -5,6 +5,7 @@ export type PlainProps = { onOpenAddRelationship: () => void; onDeleteRelationship: (clientId: string) => void; eventId: string; + programId: string; programStage: ProgramStage; ready: boolean; readOnly: boolean; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts index 418928323d..9ccddb8f3f 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import { batchActions } from 'redux-batched-actions'; import i18n from '@dhis2/d2-i18n'; +import { batchActions } from 'redux-batched-actions'; import { dataEntryIds, dataEntryKeys } from 'capture-core/constants'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { rollbackAssignee, setAssignee } from './viewEvent.actions'; import { cancelEditEventDataEntry } from '../../../WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; import { ViewEventComponent } from './ViewEvent.component'; @@ -29,6 +31,8 @@ const makeMapStateToProps = () => { ? getDataEntryKey(dataEntryIds.SINGLE_EVENT, dataEntryKeys.EDIT) : getDataEntryKey(dataEntryIds.SINGLE_EVENT, dataEntryKeys.VIEW); const isUserInteractionInProgress = dataEntryHasChanges(state, currentDataEntryKey); + const programId = state.currentSelections.programId; + const eventLabel = programId ? getTermLabel(programId, 'event') : undefined; return { programStage: programStageSelector(state), eventAccess: eventAccessSelector(state), @@ -39,8 +43,12 @@ const makeMapStateToProps = () => { getAssignedUserSaveContext: () => assignedUserContextSelector(state), eventId: state.viewEventPage.eventId, isEditEventPage: eventDetailsSection.showEditEvent, - feedbackEmptyText: i18n.t('No feedback for this event yet'), - indicatorEmptyText: i18n.t('No indicator output for this event yet'), + feedbackEmptyText: eventLabel + ? tCustomTerm('No feedback for this {{eventLabel}} yet', { eventLabel }) + : i18n.t('No feedback yet'), + indicatorEmptyText: eventLabel + ? tCustomTerm('No indicator output for this {{eventLabel}} yet', { eventLabel }) + : i18n.t('No indicator output yet'), programRules: programRulesSelector(state), }; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts index 34df6490ea..f050abdd95 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts @@ -1,9 +1,10 @@ import log from 'loglevel'; -import i18n from '@dhis2/d2-i18n'; import { errorCreator } from 'capture-core-utils'; import { ofType } from 'redux-observable'; import { switchMap } from 'rxjs/operators'; import { getErrorMessageAndDetails } from '../../../../utils/errors/getErrorMessageAndDetails'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { actionTypes as editEventActionTypes, eventFromUrlCouldNotBeRetrieved, @@ -22,11 +23,12 @@ export const getEventFromUrlEpic = ( const eventId = action.payload.eventId; const orgUnit = action.payload.orgUnit; const prevProgramId = store.value.currentSelections.programId; + const eventLabel = getTermLabel(prevProgramId, 'event'); return getEvent(eventId, absoluteApiPath, querySingleResource) .then((eventContainer: any) => { if (!eventContainer) { return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + tCustomTerm('{{eventLabel}} could not be loaded. Are you sure it exists?', { eventLabel })); } return eventFromUrlRetrieved(eventContainer, orgUnit, prevProgramId); }) @@ -35,8 +37,8 @@ export const getEventFromUrlEpic = ( log.error( errorCreator( message || - i18n.t('Event could not be loaded'))(details)); + tCustomTerm('{{eventLabel}} could not be loaded', { eventLabel }))(details)); return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + tCustomTerm('{{eventLabel}} could not be loaded. Are you sure it exists?', { eventLabel })); }); })); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 3b64fb4f55..b02f120787 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -295,9 +295,10 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId, program }: any) => ({ dataEntryKey, dataEntryId, + programId: program.id, }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts index c58e083df5..d43148063c 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts @@ -13,7 +13,7 @@ import { getTermLabel } from '../../../../../../metaData/helpers/customLabels'; const mapStateToProps = (state: ReduxState, props: { searchId: string }) => { const searchId = props.searchId; const teiSearch = (state as any).teiSearch[searchId]; - const programId = teiSearch.selectedProgramId; + const programId: string | undefined = teiSearch.selectedProgramId; const filteredRoots = getOrgUnitRoots(searchId); const roots = filteredRoots || getOrgUnitRoots('searchRoots'); diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index 71ee21ba10..365df6e4e0 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -30,9 +30,11 @@ const getProgramStageMessage = ( ? tCustomTerm('You only have view access to these {{programStagesLabel}}', { programStagesLabel }) : tCustomTerm('You only have view access to this {{programStageLabel}}', { programStageLabel })); -const getExpiredMessage = (): string => i18n.t('This event is outside the editing period'); +const getExpiredMessage = (eventLabel: string): string => + tCustomTerm('This {{eventLabel}} is outside the editing period', { eventLabel }); -const getCompletedEventMessage = (): string => i18n.t('This event has been completed'); +const getCompletedEventMessage = (eventLabel: string): string => + tCustomTerm('This {{eventLabel}} has been completed', { eventLabel }); const getDeactivatedMessage = (trackedEntityName: string | undefined): string => (trackedEntityName ? i18n.t('This {{trackedEntityName}} is deactivated', { trackedEntityName, escapeValue: false }) @@ -50,15 +52,16 @@ const getReadOnlyMessage = ({ enrollmentLabel, programStageLabel, programStagesLabel, + eventLabel, }: ReadOnlyMessageInput): string => { if (trackedEntityInactive) return getDeactivatedMessage(trackedEntityName); if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(enrollmentLabel); if (!access.program) return getProgramMessage(); if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); if (!access.programStage) return getProgramStageMessage(multipleStages, programStageLabel, programStagesLabel); - if (!eventWithinValidPeriod) return getExpiredMessage(); - if (!canEditCompletedEvent) return getCompletedEventMessage(); - if (!withinCompleteEventsExpiry) return getExpiredMessage(); + if (!eventWithinValidPeriod) return getExpiredMessage(eventLabel); + if (!canEditCompletedEvent) return getCompletedEventMessage(eventLabel); + if (!withinCompleteEventsExpiry) return getExpiredMessage(eventLabel); return ''; }; @@ -78,6 +81,7 @@ const ReadOnlyBadgePlain = ({ const enrollmentLabel = useTermLabel('enrollment'); const programStageLabel = useTermLabel('programStage'); const programStagesLabel = useTermLabel('programStage', { plural: true }); + const eventLabel = useTermLabel('event'); const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, @@ -94,6 +98,7 @@ const ReadOnlyBadgePlain = ({ enrollmentLabel, programStageLabel, programStagesLabel, + eventLabel, }); if (!message) return null; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts index 1f783f04f9..8320e42f74 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts @@ -28,4 +28,5 @@ export type ReadOnlyMessageInput = { enrollmentLabel: string; programStageLabel: string; programStagesLabel: string; + eventLabel: string; }; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index 69c2596a9f..cd39031c63 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -6,6 +6,8 @@ import { IconButton } from 'capture-ui'; import { IconDelete16, Button, colors } from '@dhis2/ui'; import { DirectionalArrow } from '../../utils/rtl'; import type { RelationshipType } from '../../metaData'; +import { getTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import type { Relationship, Entity } from './relationships.types'; const styles: Readonly = (theme: any) => ({ @@ -63,8 +65,8 @@ const styles: Readonly = (theme: any) => ({ }, }); -const getFromNames = () => ({ - PROGRAM_STAGE_INSTANCE: i18n.t('This event'), +const getFromNames = (programId: string) => ({ + PROGRAM_STAGE_INSTANCE: tCustomTerm('This {{eventLabel}}', { eventLabel: getTermLabel(programId, 'event') }), }); type PlainProps = { @@ -78,6 +80,7 @@ type PlainProps = { currentEntityId: string; smallMainButton: boolean; relationshipsRef: (instance: any) => void; + programId: string; }; type Props = PlainProps & WithStyles; @@ -105,7 +108,7 @@ class RelationshipsPlain extends React.Component { const { onRenderConnectedEntity } = this.props; if (entity.id === this.props.currentEntityId) { - return getFromNames()[entity.type]; + return getFromNames(this.props.programId)[entity.type]; } return onRenderConnectedEntity ? onRenderConnectedEntity(entity) : entity.name; diff --git a/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx b/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx index 2a8c8fce67..8c9c5c272e 100644 --- a/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx +++ b/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx @@ -2,8 +2,9 @@ import React, { type ComponentType, useState, useEffect } from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { Button, spacers, DropdownButton, FlyoutMenu, MenuItem, SplitButton } from '@dhis2/ui'; -import { scopeTypes } from '../../metaData'; +import { scopeTypes, useTermLabel } from '../../metaData'; import { useScopeInfo } from '../../hooks/useScopeInfo'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import type { PlainProps } from './TopBarActions.types'; const styles: Readonly = { @@ -26,6 +27,7 @@ const ActionButtonsPlain = ({ openConfirmDialog, }: PlainProps & WithStyles) => { const { trackedEntityName, scopeType, programName } = useScopeInfo(selectedProgramId); + const eventLabel = useTermLabel('event', { programId: selectedProgramId ?? undefined }); const [openSearch, setOpenSearch] = useState(false); useEffect(() => { @@ -65,7 +67,7 @@ const ActionButtonsPlain = ({ trackedEntityType: trackedEntityName, interpolation: { escapeValue: false }, }) - : i18n.t('Create new event') + : tCustomTerm('Create new {{eventLabel}}', { eventLabel }) } )} diff --git a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx index b9309f7d42..c9ce7c12ce 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx +++ b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx @@ -3,6 +3,8 @@ import i18n from '@dhis2/d2-i18n'; import { Button, colors, spacers, spacersNum, UserAvatar } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { Assignee } from './WidgetAssignee.types'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; const styles = () => ({ wrapper: { @@ -34,30 +36,34 @@ type Props = { avatarId?: string; } & WithStyles; -const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, classes }: Props) => ( - assignee ? ( -
-
- {i18n.t('Assigned to')} - - {assignee.name} +const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, classes }: Props) => { + const eventLabel = useTermLabel('event'); + if (assignee) { + return ( +
+
+ {i18n.t('Assigned to')} + + {assignee.name} +
+ {!readOnly && ( + + )}
- {!readOnly && ( - - )} -
- ) : ( + ); + } + return (
- {i18n.t('No one is assigned to this event')} + {tCustomTerm('No one is assigned to this {{eventLabel}}', { eventLabel })}
{!readOnly && (
- ) -); + ); +}; export const DisplayMode = withStyles(styles)(DisplayModePlain); diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.component.tsx index 3e1c90d2a1..2c8119c43e 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.component.tsx @@ -13,29 +13,32 @@ export const CompleteModalComponent = ({ onCompleteEnrollmentAndEvents, }: PlainProps) => { const enrollmentLabel = useTermLabel('enrollment'); + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); return ( {tCustomTerm('Complete {{enrollmentLabel}}', { enrollmentLabel })}

{tCustomTerm( - 'Would you like to complete the {{enrollmentLabel}} and all active events as well?', - { enrollmentLabel }, + 'Would you like to complete the {{enrollmentLabel}} and all active {{eventsLabel}} as well?', + { enrollmentLabel, eventsLabel }, )}

{Object.keys(programStagesWithActiveEvents).length !== 0 && ( <> - {i18n.t('The following events will be completed:')} + {tCustomTerm('The following {{eventsLabel}} will be completed:', { eventsLabel })} {Object.keys(programStagesWithActiveEvents).map((key) => { const { count, name } = programStagesWithActiveEvents[key]; return (
  • - {i18n.t('{{count}} event in {{programStageName}}', { + {tCustomTerm('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel, + eventsLabel, programStageName: name, - interpolation: { escapeValue: false }, + defaultValue: '{{count}} {{eventLabel}} in {{programStageName}}', + defaultValue_plural: '{{count}} {{eventsLabel}} in {{programStageName}}', })}
@@ -46,19 +49,23 @@ export const CompleteModalComponent = ({ {Object.keys(programStagesWithoutAccess).length !== 0 && ( <> - {i18n.t('The following events will not be completed due to lack of access:')} + {tCustomTerm( + 'The following {{eventsLabel}} will not be completed due to lack of access:', + { eventsLabel }, + )} {Object.keys(programStagesWithoutAccess).map((key) => { const { count, name } = programStagesWithoutAccess[key]; return (
  • - {i18n.t('{{count}} event in {{programStageName}}', { + {tCustomTerm('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel, + eventsLabel, programStageName: name, - interpolation: { escapeValue: false }, + defaultValue: '{{count}} {{eventLabel}} in {{programStageName}}', + defaultValue_plural: '{{count}} {{eventsLabel}} in {{programStageName}}', })}
@@ -77,7 +84,10 @@ export const CompleteModalComponent = ({ primary dataTest="widget-enrollment-actions-complete-button" > - {tCustomTerm('Yes, complete {{enrollmentLabel}} and events', { enrollmentLabel })} + {tCustomTerm( + 'Yes, complete {{enrollmentLabel}} and {{eventsLabel}}', + { enrollmentLabel, eventsLabel }, + )}
{displayAutoGeneratedEventWarning && (
- {i18n.t('Existing dates for auto-generated events will not be updated.')} + {tCustomTerm('Existing dates for auto-generated {{eventsLabel}} will not be updated.', { eventsLabel })}
)}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx index 84214f7804..0ad4c732a5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx @@ -314,7 +314,7 @@ const buildNotesSettingsFn = () => { dataEntryId: props.id, }), getPropName: () => 'note', - getValidatorContainers: () => getNoteValidatorContainers(), + getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel), getMeta: () => ({ placement: placements.BOTTOM, section: dataEntrySectionNames.NOTES, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx index c7a9ae88c3..0fb8d17bb0 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx @@ -19,6 +19,7 @@ import { withCustomLabels } from '../../../HOC/withCustomLabels'; const customLabels = { orgUnitLabel: { key: 'orgUnit' }, + eventLabel: { key: 'event' }, } as const; const WrappedDataEntryComponent = withCustomLabels(customLabels)(DataEntryComponent); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts index 97deed1665..84a503f322 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts @@ -1,13 +1,12 @@ -import i18n from '@dhis2/d2-i18n'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const validateNote = (value?: string | null) => !value; -export const getNoteValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateNote, - errorMessage: i18n.t('Please add or cancel the note before saving the event'), - }, - ]; - return validatorContainers; -}; +export const getNoteValidatorContainers = (eventLabel: string) => [ + { + validator: validateNote, + errorMessage: tCustomTerm('Please add or cancel the note before saving the {{eventLabel}}', { + eventLabel, + }), + }, +]; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts index decdd919af..da555d10c9 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts @@ -7,7 +7,7 @@ import { getCategoryOptionsValidatorContainers } from '../fieldValidators/catego import type { DataEntryPropToInclude } from '../../../DataEntry/actions/dataEntryLoad.utils'; import { getTermLabel } from '../../../../metaData/helpers/customLabels'; -const buildDataEntryPropsToInclude = (orgUnitLabel: string): Array => [ +const buildDataEntryPropsToInclude = (orgUnitLabel: string, eventLabel: string): Array => [ { id: 'occurredAt', type: 'DATE', @@ -31,7 +31,7 @@ const buildDataEntryPropsToInclude = (orgUnitLabel: string): Array ({ id: `attributeCategoryOptions-${category.id}`, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx index 0bdbbbce42..a6c1aa8a87 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button } from '@dhis2/ui'; import type { Props, State } from './withDeleteButton.types'; +import { getTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const getDeleteButton = (InnerComponent: React.ComponentType) => class DeleteButtonHOC extends React.Component { @@ -17,52 +19,58 @@ const getDeleteButton = (InnerComponent: React.ComponentType) => return this.innerInstance; } - renderDeleteButton = (hasDeleteButton?: boolean) => ( - hasDeleteButton ? (
- - {this.state.isOpen && ( - { + const eventLabel = getTermLabel(this.props.programId, 'event'); + return ( + hasDeleteButton ? (
+ - - - - - )} -
) : null - ); + {i18n.t('Delete')} + + {this.state.isOpen && ( + + + {tCustomTerm('Delete {{eventLabel}}', { eventLabel })} + + + {tCustomTerm( + 'Deleting an {{eventLabel}} is permanent and cannot be undone.', + { eventLabel }, + )} + {' '} + {tCustomTerm('Are you sure you want to delete this {{eventLabel}}? ', { eventLabel })} + + + + + + + + + )} +
) : null + ); + }; render() { const { onDelete, hasDeleteButton, ...passOnProps } = this.props; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts index a1d1462bbd..4f4de11f7e 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts @@ -5,6 +5,7 @@ export type Props = { formHorizontal?: boolean; formFoundation: RenderFoundation; hasDeleteButton?: boolean; + programId: string; }; export type State = { diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx index 086660bb88..043f44d3f6 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetHeader/WidgetHeader.container.tsx @@ -11,6 +11,8 @@ import { useCategoryCombinations } from '../../DataEntryDhis2Helpers/AOC/useCate import { OverflowButton } from '../../Buttons'; import { inMemoryFileStore } from '../../DataEntry/file/inMemoryFileStore'; import type { PlainProps } from './WidgetHeader.types'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const styles: Readonly = { icon: { @@ -47,6 +49,7 @@ const WidgetHeaderPlain = ({ const { programCategory } = useCategoryCombinations(programId); const { icon, name } = stage; + const eventLabel = useTermLabel('event', { programId }); return ( <> @@ -73,7 +76,7 @@ const WidgetHeaderPlain = ({ onClick={() => dispatch(startShowEditEventDataEntry(orgUnit, programCategory))} data-test="widget-enrollment-event-edit-button" > - {i18n.t('Edit event')} + {tCustomTerm('Edit {{eventLabel}}', { eventLabel })} )} diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts index 7e62d9c502..8c1c2c216d 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts @@ -13,14 +13,15 @@ export const batchActionTypes = { ADD_NOTE_BATCH_FOR_EVENT: 'AddNoteBatchForEvent', }; -export const requestAddNoteForEvent = (itemId: string, dataEntryId: string, note: string) => - actionCreator(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT)({ itemId, dataEntryId, note }); +export const requestAddNoteForEvent = (itemId: string, dataEntryId: string, note: string, programId: string) => + actionCreator(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT)({ itemId, dataEntryId, note, programId }); export const startAddNoteForEvent = ( eventUid: string, serverData: Record, selections: Record, context: Record, + programId: string, ) => actionCreator(actionTypes.START_ADD_NOTE_FOR_EVENT)({ selections, context }, { offline: { @@ -32,6 +33,9 @@ export const startAddNoteForEvent = ( data: serverData, }, commit: { type: actionTypes.NOTE_ADDED_FOR_EVENT, meta: { selections, context } }, - rollback: { type: actionTypes.ADD_NOTE_FAILED_FOR_EVENT, meta: { selections, context } }, + rollback: { + type: actionTypes.ADD_NOTE_FAILED_FOR_EVENT, + meta: { selections, context, programId }, + }, }, }); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index d2306069a7..3a805bec72 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -1,13 +1,14 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; -export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { +export const WidgetEventNote = ({ dataEntryKey, dataEntryId, programId }: Props) => { const dispatch = useDispatch(); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -16,17 +17,18 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { trackedEntityTypeName, showWidgetBadge, } = useEnrollmentAccessContext(); + const eventLabel = useTermLabel('event'); const onAddNote = (newNoteValue: string) => { - dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue)); + dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue, programId)); }; return (
{ + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); if (!scheduleDate || !suggestedScheduleDate) { return null; } @@ -58,17 +62,20 @@ const InfoBoxPlain = ({ {!!orgUnitName && ( <> {' '} - {i18n.t('There are {{count}} scheduled event in this program in {{orgUnitName}} on this day.', { - count: eventCountInOrgUnit, - orgUnitName, - // eslint-disable-next-line max-len - defaultValue: 'There are {{count}} scheduled event in this program in {{orgUnitName}} on this day.', - // eslint-disable-next-line max-len - defaultValue_plural: 'There are {{count}} scheduled events in this program in {{orgUnitName}} on this day.', - interpolation: { - escapeValue: false, + {tCustomTerm( + 'There are {{count}} scheduled {{eventLabel}} in this program ' + + 'in {{orgUnitName}} on this day.', + { + count: eventCountInOrgUnit, + orgUnitName, + eventLabel, + eventsLabel, + defaultValue: 'There are {{count}} scheduled {{eventLabel}} in this program ' + + 'in {{orgUnitName}} on this day.', + defaultValue_plural: 'There are {{count}} scheduled {{eventsLabel}} in this program ' + + 'in {{orgUnitName}} on this day.', }, - })} + )} )} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx index ef5a063a4d..89da423d1f 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx @@ -101,11 +101,11 @@ const ScheduleDatePlain = ({ if (!isWithinValidPeriod) { return { error: true, - // eslint-disable-next-line max-len - validationText: i18n.t('The date entered belongs to an expired period. Enter a date after {{firstValidDate}}.', { - firstValidDate, - interpolation: { escapeValue: false }, - }), + validationText: i18n.t( + 'The date entered belongs to an expired period. ' + + 'Enter a date after {{firstValidDate}}.', + { firstValidDate, interpolation: { escapeValue: false } }, + ), }; } diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/ScheduleText.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/ScheduleText.component.tsx index 502d5a5c91..bcf2bf0d7d 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/ScheduleText.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/ScheduleText.component.tsx @@ -1,16 +1,20 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { InfoIconText } from '../../InfoIconText'; import type { Props } from './scheduleText.types'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; -export const ScheduleText = ({ orgUnitName, stageName, programName }: Props) => ( - - - {orgUnitName - ? i18n.t('Scheduling an event in {{stageName}} for {{programName}} in {{orgUnitName}}', - { orgUnitName, stageName, programName, interpolation: { escapeValue: false } }) - : i18n.t('Scheduling an event in {{stageName}} for {{programName}}', - { stageName, programName, interpolation: { escapeValue: false } })} - - -); +export const ScheduleText = ({ orgUnitName, stageName, programName }: Props) => { + const eventLabel = useTermLabel('event'); + return ( + + + {orgUnitName + ? tCustomTerm('Scheduling an {{eventLabel}} in {{stageName}} for {{programName}} in {{orgUnitName}}', + { orgUnitName, stageName, programName, eventLabel }) + : tCustomTerm('Scheduling an {{eventLabel}} in {{stageName}} for {{programName}}', + { stageName, programName, eventLabel })} + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx index d3e3fb6a74..fc067892e8 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx @@ -14,6 +14,8 @@ import type { Props } from './widgetEventSchedule.types'; import { CategoryOptions } from './CategoryOptions/CategoryOptions.component'; import { Assignee } from './Assignee'; import { ScheduleOrgUnit } from './ScheduleOrgUnit/ScheduleOrgUnit.component'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; const styles = (theme: any) => ({ wrapper: { @@ -73,6 +75,7 @@ const WidgetEventSchedulePlain = ({ const formIsValid = () => Boolean(isValidOrgUnit(orgUnit) && scheduleDate && !validation?.error); setIsFormValid(formIsValid()); }, [orgUnit, scheduleDate, validation, setIsFormValid]); + const eventLabel = useTermLabel('event', { programId }); return ( } diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/LinkToExisting/LinkToExisting.component.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/LinkToExisting/LinkToExisting.component.tsx index 62a97b6429..925be74d22 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/LinkToExisting/LinkToExisting.component.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/LinkToExisting/LinkToExisting.component.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { SingleSelectField, withDefaultFieldContainer, @@ -9,6 +8,8 @@ import { import labelTypeClasses from '../FormComponents/dataEntryFieldLabels.module.css'; import { baseInputStyles } from '../FormComponents/commonProps'; import type { LinkToExistingProps } from './LinkToExisting.types'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const SingleSelectForForm = withDefaultFieldContainer()( withLabel({ @@ -29,6 +30,7 @@ export const LinkToExisting = ({ saveAttempted, }: LinkToExistingProps) => { const [touched, setTouched] = useState(false); + const eventLabel = useTermLabel('event'); const handleChange = (value: string | null) => { setTouched(true); @@ -47,8 +49,9 @@ export const LinkToExisting = ({ label: event.label, })); - const label = i18n.t('Choose a {{linkableStageLabel}} event', { + const label = tCustomTerm('Choose a {{linkableStageLabel}} {{eventLabel}}', { linkableStageLabel, + eventLabel, }); const shouldShowError = (saveAttempted || touched); @@ -61,7 +64,7 @@ export const LinkToExisting = ({ onChange={handleChange} onBlur={handleBlur} options={options} - placeholder={i18n.t('Select an event')} + placeholder={tCustomTerm('Select an {{eventLabel}}', { eventLabel })} clearable styles={baseInputStyles} errorMessage={shouldShowError ? errorMessages.linkedEventId : undefined} diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx index 3791e98e55..e80faf3656 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx @@ -11,6 +11,8 @@ import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollec import type { PlainProps, LinkButtonProps } from './RelatedStagesActions.types'; import { LinkToExisting } from '../LinkToExisting'; import { EnterDataInOrgUnit } from '../EnterDataInOrgUnit/EnterData.component'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const styles: Readonly = { wrapper: { @@ -49,6 +51,7 @@ const Schedule = ({ programStage, canAddNewEventToStage, }) => { + const eventLabel = useTermLabel('event'); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.SCHEDULE_IN_ORG] || {}; if (hidden) { @@ -60,9 +63,9 @@ const Schedule = ({ if (disabled) { tooltipContent = disabledMessage; } else { - tooltipContent = i18n.t('{{ linkableStageLabel }} can only have one event', { + tooltipContent = tCustomTerm('{{ linkableStageLabel }} can only have one {{eventLabel}}', { linkableStageLabel: programStage.stageForm.name, - interpolation: { escapeValue: false }, + eventLabel, }); } @@ -93,6 +96,7 @@ const EnterData = ({ programStage, canAddNewEventToStage, }) => { + const eventLabel = useTermLabel('event'); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.ENTER_DATA] || {}; if (hidden) { @@ -104,9 +108,9 @@ const EnterData = ({ if (disabled) { tooltipContent = disabledMessage; } else { - tooltipContent = i18n.t('{{ linkableStageLabel }} can only have one event', { + tooltipContent = tCustomTerm('{{ linkableStageLabel }} can only have one {{eventLabel}}', { linkableStageLabel: programStage.stageForm.name, - interpolation: { escapeValue: false }, + eventLabel, }); } @@ -137,6 +141,8 @@ const LinkExistingResponse = ({ updateSelectedAction, programStage, }) => { + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.LINK_EXISTING_RESPONSE] || {}; if (hidden) { @@ -148,9 +154,9 @@ const LinkExistingResponse = ({ if (disabled) { tooltipContent = disabledMessage; } else if (!linkableEvents.length) { - tooltipContent = i18n.t('{{ linkableStageLabel }} has no linkable events', { + tooltipContent = tCustomTerm('{{ linkableStageLabel }} has no linkable {{eventsLabel}}', { linkableStageLabel: programStage.stageForm.name, - interpolation: { escapeValue: false }, + eventsLabel, }); } @@ -165,7 +171,7 @@ const LinkExistingResponse = ({ name={`related-stage-action-${relatedStageActions.LINK_EXISTING_RESPONSE}`} checked={relatedStageActions.LINK_EXISTING_RESPONSE === selectedAction} disabled={tooltipEnabled} - label={mainOptionTranslatedTexts[relatedStageActions.LINK_EXISTING_RESPONSE]} + label={tCustomTerm('Link to an existing {{eventLabel}}', { eventLabel })} onChange={e => updateSelectedAction(e.value)} value={relatedStageActions.LINK_EXISTING_RESPONSE} dataTest="related-stages-actions-link-existing-response" diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx index eeed2858bf..ae76ae12ad 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -1,7 +1,6 @@ import React, { useRef, useCallback, useState } from 'react'; import { IconLink24, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import i18n from '@dhis2/d2-i18n'; import { Widget } from '../Widget'; import { RelatedStagesActions } from './RelatedStagesActions'; import { useLinkedEventByOriginId } from '../WidgetTwoEventWorkspace/hooks'; @@ -16,6 +15,8 @@ import { relatedStageStatus } from './constants'; import { useCommonEnrollmentDomainData } from '../Pages/common/EnrollmentOverviewDomain'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { RequestEvent } from '../DataEntries'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; const styles = { header: { @@ -48,6 +49,7 @@ export const WidgetRelatedStagesPlain = ({ classes, }: Props) => { const [isLinking, setIsLinking] = useState(false); + const eventLabel = useTermLabel('event', { programId }); const { enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { currentRelatedStagesStatus, constraint } = useRelatedStages({ programStageId, programId }); const { stageWriteAccessById } = useEnrollmentAccessContext(); @@ -123,7 +125,7 @@ export const WidgetRelatedStagesPlain = ({ - {i18n.t('Linked event')} + {tCustomTerm('Linked {{eventLabel}}', { eventLabel })}
} > diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts index 08d2aa16e2..74c218780b 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts @@ -9,7 +9,6 @@ export const relatedStageActions = Object.freeze({ export const mainOptionTranslatedTexts = { [relatedStageActions.SCHEDULE_IN_ORG]: i18n.t('Schedule'), [relatedStageActions.ENTER_DATA]: i18n.t('Enter details now'), - [relatedStageActions.LINK_EXISTING_RESPONSE]: i18n.t('Link to an existing event'), }; export const relatedStageStatus = Object.freeze({ diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts index 4dbc628392..95b1350f93 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts @@ -1,7 +1,8 @@ -import i18n from '@dhis2/d2-i18n'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { relatedStageActions } from '../constants'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; const ReactQueryAppNamespace = 'capture'; @@ -30,6 +31,7 @@ export const useAddEventWithRelationship = ({ const queryClient = useQueryClient(); const { show: showSuccess } = useAlert(({ message }) => message, { success: true }); const { show: showAlert } = useAlert(({ message }) => message, { critical: true }); + const eventLabel = useTermLabel('event'); const { mutate } = useMutation( ({ serverData }: { serverData: any }) => @@ -55,12 +57,12 @@ export const useAddEventWithRelationship = ({ if (payload.linkMode === relatedStageActions.ENTER_DATA && payload.eventIdToRedirectTo) { onNavigateToEvent(payload.eventIdToRedirectTo); } else { - showSuccess({ message: i18n.t('The event was successfully linked') }); + showSuccess({ message: tCustomTerm('The {{eventLabel}} was successfully linked', { eventLabel }) }); } }, onError: (_, payload: { serverData: Record }) => { setIsLinking(false); - showAlert({ message: i18n.t('An error occurred while linking the event') }); + showAlert({ message: tCustomTerm('An error occurred while linking the {{eventLabel}}', { eventLabel }) }); onUpdateEnrollmentEventsError && onUpdateEnrollmentEventsError((payload.serverData as any).events); }, }, diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index 198a2e104f..aacc936b0d 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react'; import { Button, IconAdd16 } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../../../../Tooltips/ConditionalTooltip'; import { useTermLabel } from '../../../../../metaData'; import { tCustomTerm } from '../../../../../utils/tCustomTerm'; @@ -21,27 +20,32 @@ export const StageCreateNewButton = ({ eventName, }: Props) => { const programStageLabel = useTermLabel('programStage'); + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); const { isDisabled, tooltipContent } = useMemo(() => { if (preventAddingEventActionInEffect) { return { isDisabled: true, - tooltipContent: i18n.t("You can't add any more {{ programStageName }} events", { + tooltipContent: tCustomTerm("You can't add any more {{ programStageName }} {{eventsLabel}}", { programStageName: eventName, - interpolation: { escapeValue: false }, + eventsLabel, }), }; } if (!repeatable && eventCount > 0) { return { isDisabled: true, - tooltipContent: tCustomTerm('This {{programStageLabel}} can only have one event', { programStageLabel }), + tooltipContent: tCustomTerm( + 'This {{programStageLabel}} can only have one {{eventLabel}}', + { programStageLabel, eventLabel }, + ), }; } return { isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, programStageLabel]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, programStageLabel, eventLabel, eventsLabel]); return ( - {i18n.t('New {{ eventName }} event', { - eventName, interpolation: { escapeValue: false }, + {tCustomTerm('New {{ eventName }} {{eventLabel}}', { + eventName, eventLabel, })} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx index cd1ee4f139..64927015ff 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx @@ -7,8 +7,9 @@ import { } from '@dhis2/ui'; import { ConditionalTooltip } from '../../../../../../Tooltips/ConditionalTooltip'; import { convertClientToView, convertServerToClient } from '../../../../../../../converters'; -import { dataElementTypes, type ProgramStage } from '../../../../../../../metaData'; +import { dataElementTypes, type ProgramStage, useTermLabel } from '../../../../../../../metaData'; import { useEventEditPermissions } from '../../../../../../../hooks'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; type Props = { setActionsOpen: (open: boolean) => void; @@ -31,6 +32,7 @@ export const DeleteActionButton = ({ }: Props) => { const occurredAtClient = convertServerToClient(occurredAt, dataElementTypes.DATE) as string; const occurredAtClientView = convertClientToView(occurredAtClient, dataElementTypes.DATE); + const eventLabel = useTermLabel('event', { programId }); const { isEventWithinValidPeriod, @@ -46,15 +48,15 @@ export const DeleteActionButton = ({ const getDisabledMessage = (): string => { if (!isEventWithinValidPeriod) { - return i18n.t('{{occurredAt}} belongs to an expired period. Event cannot be deleted', { + return tCustomTerm('{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be deleted', { occurredAt: occurredAtClientView, - interpolation: { escapeValue: false }, + eventLabel, }); } if (!canEditCompletedEvent) { - return i18n.t('This event has been completed'); + return tCustomTerm('This {{eventLabel}} has been completed', { eventLabel }); } - return i18n.t('This event is outside the edit period'); + return tCustomTerm('This {{eventLabel}} is outside the edit period', { eventLabel }); }; return ( diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx index 2883536f0f..938689d4a9 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx @@ -6,6 +6,8 @@ import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useMutation } from '@tanstack/react-query'; import { errorCreator } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; +import { useTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; type Props = { eventId: string; @@ -24,6 +26,7 @@ export const DeleteActionModal = ({ onDeleteEvent, onRollbackDeleteEvent, }: Props) => { + const eventLabel = useTermLabel('event'); const { show: showError } = useAlert( ({ message }) => message, { @@ -54,7 +57,7 @@ export const DeleteActionModal = ({ return eventToRollbackOnFail; }, onError: (apiError: unknown, payload: unknown, eventToRollbackOnFail?: ApiEnrollmentEvent) => { - showError({ message: i18n.t('An error occurred while deleting the event') }); + showError({ message: tCustomTerm('An error occurred while deleting the {{eventLabel}}', { eventLabel }) }); log.error(errorCreator('An error occurred while deleting the event')({ apiError, payload })); if (eventToRollbackOnFail) { @@ -70,13 +73,13 @@ export const DeleteActionModal = ({ small > - {i18n.t('Delete event')} + {tCustomTerm('Delete {{eventLabel}}', { eventLabel })}

- {i18n.t('Deleting an event is permanent and cannot be undone.')} + {tCustomTerm('Deleting an {{eventLabel}} is permanent and cannot be undone.', { eventLabel })} {' '} - {i18n.t('Are you sure you want to delete this event?')} + {tCustomTerm('Are you sure you want to delete this {{eventLabel}}?', { eventLabel })}

@@ -90,7 +93,7 @@ export const DeleteActionModal = ({ destructive onClick={() => !pendingApiResponse && mutate({ eventId })} > - {i18n.t('Yes, delete event')} + {tCustomTerm('Yes, delete {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx index 322c291ec1..0bdb8c69df 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx @@ -11,6 +11,8 @@ import { errorCreator } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; import { DirectionalArrow } from '../../../../../../../utils/rtl'; import { EventStatuses } from '../EventRow'; +import { useTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; type Props = { eventId: string; @@ -28,6 +30,7 @@ export const SkipAction = ({ onUpdateEventStatus, }: Props) => { const dataEngine = useDataEngine(); + const eventLabel = useTermLabel('event'); const { show: showError } = useAlert( ({ message }) => message, { critical: true }, @@ -56,7 +59,7 @@ export const SkipAction = ({ return { previousStatus }; }, onError: (error: unknown, payload: { status: string }, context?: { previousStatus: string }) => { - showError({ message: i18n.t('An error occurred when updating event status') }); + showError({ message: tCustomTerm('An error occurred when updating {{eventLabel}} status', { eventLabel }) }); log.error(errorCreator('An error occurred when updating event status')({ error, payload, context })); context && onUpdateEventStatus(eventId, context.previousStatus); }, diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index ada5786b24..57f0bc1da9 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx @@ -23,6 +23,8 @@ import { StageCreateNewButton } from '../StageCreateNewButton'; import { useComputeDataFromEvent, useComputeHeaderColumn, formatRowForView } from './hooks/useEventList'; import { DEFAULT_NUMBER_OF_ROW, SORT_DIRECTION } from './hooks/constants'; import { getProgramAndStageForProgram } from '../../../../../metaData/helpers'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import type { Props } from './stageDetail.types'; import { EventRow } from './EventRow'; import { useClientDataElements } from './hooks/useClientDataElements'; @@ -107,6 +109,8 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); + const eventLabel = useTermLabel('event', { programId }); + const eventsLabel = useTermLabel('event', { programId, plural: true }); const { stageWriteAccessById } = useEnrollmentAccessContext(); const stageWriteAccess = stageWriteAccessById[stageId] ?? stage?.access?.data?.write; const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); @@ -180,7 +184,10 @@ const StageDetailPlain = (props: Props & WithStyles) => { const cells = headerColumns.map(({ id }) => ( {({ onMouseOver, onMouseOut, ref }) => ( @@ -281,7 +288,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { if (error) { return (
- {i18n.t('Events could not be retrieved. Please try again later.')} + {tCustomTerm('{{eventsLabel}} could not be retrieved. Please try again later.', { eventsLabel })}
); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index d87ef71abc..910e5d4275 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -15,7 +15,8 @@ import { useEnrollmentAccessContext } from '../../../../Pages/common/EnrollmentO import type { Props } from './stageOverview.types'; import { isEventOverdue } from '../StageDetail/hooks/helpers'; import { convertValue as convertValueClientToView } from '../../../../../converters/clientToView'; -import { dataElementTypes } from '../../../../../metaData'; +import { dataElementTypes, useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; const styles: Readonly = { container: { @@ -101,6 +102,8 @@ export const StageOverviewPlain = ({ const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; const scheduledEvents = events.filter(event => event.status === statusTypes.SCHEDULE).length; + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); return (
@@ -136,10 +139,12 @@ export const StageOverviewPlain = ({
- {i18n.t('{{ count }} event', { + {tCustomTerm('{{count}} {{eventLabel}}', { count: totalEvents, - defaultValue: '{{ count }} event', - defaultValue_plural: '{{count}} events', + eventLabel, + eventsLabel, + defaultValue: '{{count}} {{eventLabel}}', + defaultValue_plural: '{{count}} {{eventsLabel}}', })}
{overdueEvents > 0 ?
diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx index 0a3ce4e9f8..3e809c71b6 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx @@ -13,6 +13,8 @@ import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers'; import type { Props } from './UnlinkAndDeleteModal.types'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; export const UnlinkAndDeleteModal = ({ setOpenModal, @@ -24,8 +26,9 @@ export const UnlinkAndDeleteModal = ({ }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const eventLabel = useTermLabel('event'); const { show: showErrorAlert } = useAlert( - i18n.t('An error occurred while unlinking and deleting the event.'), + tCustomTerm('An error occurred while unlinking and deleting the {{eventLabel}}.', { eventLabel }), { critical: true }, ); @@ -61,12 +64,18 @@ export const UnlinkAndDeleteModal = ({ return ( - {i18n.t('Unlink and delete linked event')} + {tCustomTerm('Unlink and delete linked {{eventLabel}}', { eventLabel })}

- {i18n.t('Are you sure you want to remove the link and delete the linked event?')} + {tCustomTerm( + 'Are you sure you want to remove the link and delete the linked {{eventLabel}}?', + { eventLabel }, + )} {' '} - {i18n.t('This action permanently removes the link, linked event, and all related data.')} + {tCustomTerm( + 'This action permanently removes the link, linked {{eventLabel}}, and all related data.', + { eventLabel }, + )}

@@ -82,7 +91,7 @@ export const UnlinkAndDeleteModal = ({ onClick={() => mutation.mutate()} disabled={mutation.isLoading} > - {i18n.t('Yes, unlink and delete linked event')} + {tCustomTerm('Yes, unlink and delete linked {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx index 9c96cea626..41fea724c2 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx @@ -13,6 +13,8 @@ import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers'; import type { Props } from './UnlinkModal.types'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; export const UnlinkModal = ({ setOpenModal, @@ -22,8 +24,10 @@ export const UnlinkModal = ({ }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); const { show: showErrorAlert } = useAlert( - i18n.t('An error occurred while unlinking and deleting the event.'), + tCustomTerm('An error occurred while unlinking and deleting the {{eventLabel}}.', { eventLabel }), { critical: true }, ); @@ -59,13 +63,16 @@ export const UnlinkModal = ({ return ( - {i18n.t('Unlink event')} + {tCustomTerm('Unlink {{eventLabel}}', { eventLabel })}

- {i18n.t('Are you sure you want to remove the link between these events?')} + {tCustomTerm('Are you sure you want to remove the link between these {{eventsLabel}}?', { eventsLabel })} {' '} - {i18n.t('This action removes the link itself, but the linked event will remain.')} + {tCustomTerm( + 'This action removes the link itself, but the linked {{eventLabel}} will remain.', + { eventLabel }, + )}

@@ -79,7 +86,7 @@ export const UnlinkModal = ({ disabled={mutation.isLoading} dataTest="event-overflow-unlink-event-confirm" > - {i18n.t('Yes, unlink event')} + {tCustomTerm('Yes, unlink {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx index 11f07eb94b..549c2cdf80 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx @@ -8,13 +8,14 @@ import { IconView16, MenuItem, } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip'; import { OverflowButton } from '../../Buttons'; import { UnlinkModal, UnlinkAndDeleteModal } from './Modal'; import { useNavigate, buildUrlQueryString } from '../../../utils/routing'; import type { Props } from './OverflowMenu.types'; import { useRelationshipTypeAccess } from '../hooks'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const OverflowMenuComponent = ({ linkedEvent, @@ -31,6 +32,8 @@ export const OverflowMenuComponent = ({ const [isUnlinkModalOpen, setIsUnlinkModalOpen] = useState(false); const [isUnlinkAndDeleteModalOpen, setIsUnlinkAndDeleteModalOpen] = useState(false); const { relationshipTypeWriteAccess } = useRelationshipTypeAccess(relationshipType); + const eventLabel = useTermLabel('event'); + const eventsLabel = useTermLabel('event', { plural: true }); const handleViewLinkedEvent = () => { navigate(`/enrollmentEventEdit?${buildUrlQueryString({ eventId: linkedEvent.event, orgUnitId })}`); @@ -59,7 +62,7 @@ export const OverflowMenuComponent = ({ component={ } dataTest="event-overflow-view-linked-event" onClick={handleViewLinkedEvent} @@ -67,11 +70,14 @@ export const OverflowMenuComponent = ({ /> } disabled={!stageWriteAccess || !relationshipTypeWriteAccess} dense @@ -81,11 +87,14 @@ export const OverflowMenuComponent = ({ /> } disabled={!stageWriteAccess || !relationshipTypeWriteAccess} dense diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx index 4f08e0af71..00e6e5c3b6 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { colors, spacersNum, IconLink16 } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { PlainProps } from './WidgetWrapper.types'; import { WidgetTwoEventWorkspaceWrapperTypes } from '../index'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const styles: Readonly = { container: { @@ -40,6 +41,7 @@ export const styles: Readonly = { }; const WidgetWrapperPlain = ({ widget, type, stage, linkedStage, classes }: PlainProps & WithStyles) => { + const eventLabel = useTermLabel('event'); if (type === WidgetTwoEventWorkspaceWrapperTypes.EDIT_EVENT) { return (
@@ -49,16 +51,17 @@ const WidgetWrapperPlain = ({ widget, type, stage, linkedStage, classes }: Plain -
{i18n.t('Linked event')}
+
{tCustomTerm('Linked {{eventLabel}}', { eventLabel })}
{linkedStage?.name && stage?.name ? - // eslint-disable-next-line max-len - i18n.t('This {{stageName}} event is linked to a {{linkedStageName}} event. Review the linked event details before entering data below', + tCustomTerm( + 'This {{stageName}} {{eventLabel}} is linked to a {{linkedStageName}} {{eventLabel}}. ' + + 'Review the linked {{eventLabel}} details before entering data below', { linkedStageName: linkedStage.name, stageName: stage.name, - interpolation: { escapeValue: false }, + eventLabel, }, ) : ''}
diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx index 742e9f6bab..4b6ad01e50 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx @@ -45,8 +45,8 @@ export const EventWorkingListsReduxProvider = ({ storeId, program, programStage, }, [dispatch, contextOrgUnitId]); const onDeleteEvent = useCallback((eventId: string) => { - dispatch(requestDeleteEvent(eventId, storeId)); - }, [dispatch, storeId]); + dispatch(requestDeleteEvent(eventId, storeId, program.id)); + }, [dispatch, storeId, program.id]); const getLockedFilters = useCallback((selectedTemplate: any) => { if (!selectedTemplate.isDefault) { diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx index 517fd592e1..92acf83b7b 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx @@ -1,6 +1,8 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } from '@dhis2/ui'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; type Props = { eventId: string; @@ -9,6 +11,7 @@ type Props = { }; export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) => { + const eventLabel = useTermLabel('event'); const handleConfirm = () => { onConfirmDelete(eventId); onClose(); @@ -20,13 +23,13 @@ export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) = small > - {i18n.t('Delete event')} + {tCustomTerm('Delete {{eventLabel}}', { eventLabel })}

- {i18n.t('Deleting an event is permanent and cannot be undone.')} + {tCustomTerm('Deleting an {{eventLabel}} is permanent and cannot be undone.', { eventLabel })} {' '} - {i18n.t('Are you sure you want to delete this event?')} + {tCustomTerm('Are you sure you want to delete this {{eventLabel}}?', { eventLabel })}

@@ -40,7 +43,7 @@ export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) = destructive onClick={handleConfirm} > - {i18n.t('Yes, delete event')} + {tCustomTerm('Yes, delete {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx index b85a4c0299..c6eb8aeab7 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx @@ -1,5 +1,4 @@ import React, { useMemo, useState } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { IconDelete24, colors } from '@dhis2/ui'; import { EventWorkingListsUpdateTrigger } from '../UpdateTrigger'; import type { CustomRowMenuContents } from '../../WorkingListsBase'; @@ -7,10 +6,13 @@ import type { Props } from './eventWorkingListsRowMenuSetup.types'; import { useProgramExpiryForUser } from '../../../../hooks'; import { isValidPeriod } from '../../../../utils/validation/validators/form'; import { DeleteEventModal } from './DeleteEventModal'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...passOnProps }: Props) => { const expiryPeriod = useProgramExpiryForUser(programId); + const eventLabel = useTermLabel('event', { programId }); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [eventIdToDelete, setEventIdToDelete] = useState(null); @@ -32,15 +34,15 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas key: 'deleteEventItem', clickHandler: ({ id }) => handleOpenDeleteModal(id), icon: , - label: i18n.t('Delete event'), + label: tCustomTerm('Delete {{eventLabel}}', { eventLabel }), tooltipContent: (row) => { const { occurredAt } = row ?? {}; const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); - return isWithinValidPeriod ? null : i18n.t( - '{{occurredAt}} belongs to an expired period. Event cannot be deleted', + return isWithinValidPeriod ? null : tCustomTerm( + '{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be deleted', { occurredAt, - interpolation: { escapeValue: false }, + eventLabel, }, ); }, @@ -54,7 +56,7 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); return !isWithinValidPeriod; }, - }], [expiryPeriod]); + }], [expiryPeriod, eventLabel]); return ( diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts index 65536e3865..a48b6d2280 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts @@ -120,7 +120,7 @@ export const requestDeleteEventEpic = ( action$.pipe( ofType(actionTypes.EVENT_REQUEST_DELETE), concatMap((action) => { - const { eventId, storeId } = action.payload; + const { eventId, storeId, programId } = action.payload; const deletePromise = mutate({ resource: 'tracker?async=false&importStrategy=DELETE', type: 'create', @@ -131,7 +131,7 @@ export const requestDeleteEventEpic = ( .then(() => deleteEventSuccess(eventId, storeId)) .catch((error) => { log.error(errorCreator('Could not delete event')({ error, eventId })); - return deleteEventError(); + return deleteEventError(programId); }); return from(deletePromise).pipe( diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts index 832e532a64..19c5fb8869 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts @@ -12,10 +12,10 @@ export const deleteEventSuccess = (eventId: string, storeId: string) => actionCreator(actionTypes.EVENT_DELETE_SUCCESS)({ eventId, storeId }); export const deleteEventError = - () => actionCreator(actionTypes.EVENT_DELETE_ERROR)(); + (programId: string) => actionCreator(actionTypes.EVENT_DELETE_ERROR)(null, { programId }); export const openViewEventPage = (eventId: string, contextOrgUnitId: string | null | undefined) => actionCreator(actionTypes.VIEW_EVENT_PAGE_OPEN)({ eventId, orgUnitId: contextOrgUnitId }); -export const requestDeleteEvent = (eventId: string, storeId: string) => - actionCreator(actionTypes.EVENT_REQUEST_DELETE)({ eventId, storeId }); +export const requestDeleteEvent = (eventId: string, storeId: string, programId: string) => + actionCreator(actionTypes.EVENT_REQUEST_DELETE)({ eventId, storeId, programId }); diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx index 35de3c8129..6d811038a4 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx @@ -6,6 +6,8 @@ import { useBulkCompleteEvents } from './hooks/useBulkCompleteEvents'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import { Widget } from '../../../../../Widget'; import type { Props } from './CompleteAction.types'; +import { getTermLabel, useTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const styles: Readonly = { container: { @@ -21,9 +23,15 @@ const styles: Readonly = { }, }; -const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { +const getTooltipContent = ( + stageDataWriteAccess: boolean | undefined, + bulkDataEntryIsActive: boolean | undefined, + programId: string, +) => { if (!stageDataWriteAccess) { - return i18n.t('You do not have access to complete events'); + return tCustomTerm('You do not have access to complete {{eventsLabel}}', { + eventsLabel: getTermLabel(programId, 'event', { plural: true }), + }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -42,7 +50,8 @@ const CompleteActionPlain = ({ }: Props & WithStyles) => { const [isCompleteDialogOpen, setIsCompleteDialogOpen] = useState(false); const [openAccordion, setOpenAccordion] = useState(false); - const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive); + const eventsLabel = useTermLabel('event', { programId, plural: true }); + const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive, programId); const disabled = Boolean(!stageDataWriteAccess || bulkDataEntryIsActive); const { eventCounts, @@ -81,15 +90,21 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-events-dialog'} > - {i18n.t('Complete events')} + {tCustomTerm('Complete {{eventsLabel}}', { eventsLabel })} {eventCounts.active > 0 ? - i18n.t('Are you sure you want to complete all active events in selection?') + tCustomTerm( + 'Are you sure you want to complete all active {{eventsLabel}} in selection?', + { eventsLabel }, + ) : - i18n.t('There are no active events to complete in the current selection.') + tCustomTerm( + 'There are no active {{eventsLabel}} to complete in the current selection.', + { eventsLabel }, + ) } @@ -124,12 +139,12 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-events-dialog'} > - {i18n.t('Error completing events')} + {tCustomTerm('Error completing {{eventsLabel}}', { eventsLabel })} - {i18n.t('There was an error completing the events.')} + {tCustomTerm('There was an error completing the {{eventsLabel}}.', { eventsLabel })} void; removeRowsFromSelection: (rows: Array) => void; - programId?: string; + programId: string; }; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts index 889127fb1b..d0ec0c2465 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts @@ -1,9 +1,10 @@ import { useCallback, useEffect, useMemo } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { useMutation } from '@tanstack/react-query'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; +import { useTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; type Props = { selectedRows: { [key: string]: boolean }; @@ -23,6 +24,7 @@ export const useBulkCompleteEvents = ({ programId, }: Props) => { const dataEngine = useDataEngine(); + const eventsLabel = useTermLabel('event', { programId, plural: true }); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, @@ -75,7 +77,7 @@ export const useBulkCompleteEvents = ({ }), { onError: () => { - showAlert({ message: i18n.t('An error occurred while completing events') }); + showAlert({ message: tCustomTerm('An error occurred while completing {{eventsLabel}}', { eventsLabel }) }); }, onSuccess: (response, { payload }: any) => { const errorReports = response?.validationReport?.errorReports; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx index 5eaf520b57..c9cffcab6e 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx @@ -7,10 +7,19 @@ import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator } from 'capture-core-utils'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import type { Props } from './DeleteAction.types'; +import { useTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; -const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { +const getTooltipContent = ( + stageDataWriteAccess: boolean | undefined, + bulkDataEntryIsActive: boolean | undefined, + eventsLabel: string, +) => { if (!stageDataWriteAccess) { - return i18n.t('You do not have access to delete events'); + return tCustomTerm( + 'You do not have access to delete {{eventsLabel}}', + { eventsLabel }, + ); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -26,12 +35,13 @@ export const DeleteAction = ({ }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); const dataEngine = useDataEngine(); + const eventsLabel = useTermLabel('event', { plural: true }); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, ); - const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive); + const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive, eventsLabel); const disabled = Boolean(!stageDataWriteAccess || !!bulkDataEntryIsActive); const { mutate: deleteEvents, isLoading }: { mutate: any, isLoading: boolean } = useMutation( @@ -47,7 +57,7 @@ export const DeleteAction = ({ { onError: (error) => { log.error(errorCreator('An error occurred while deleting the events')({ error })); - showAlert({ message: i18n.t('An error occurred while deleting the events') }); + showAlert({ message: tCustomTerm('An error occurred while deleting the {{eventsLabel}}', { eventsLabel }) }); }, onSuccess: () => { onUpdateList(); @@ -78,13 +88,13 @@ export const DeleteAction = ({ dataTest={'bulk-delete-events-dialog'} > - {i18n.t('Delete events')} + {tCustomTerm('Delete {{eventsLabel}}', { eventsLabel })} {i18n.t('This cannot be undone.')} {' '} - {i18n.t('Are you sure you want to delete the selected events?')} + {tCustomTerm('Are you sure you want to delete the selected {{eventsLabel}}?', { eventsLabel })} diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx index c48eddb0f9..a9ed9fd675 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx @@ -24,7 +24,7 @@ export const EventBulkActions = ({ selectedRowsCount={selectedRowsCount} onClearSelection={onClearSelection} > - {programId && onOpenBulkDataEntryPlugin && ( + {onOpenBulkDataEntryPlugin && ( void; removeRowsFromSelection: (rows: Array) => void; - programId?: string; + programId: string; onOpenBulkDataEntryPlugin?: () => void; bulkDataEntryIsActive?: boolean; }; From 9d85281a312369b675cd2251f7eb1bd709ea560f Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:34:17 +0000 Subject: [PATCH 049/118] feat: update data loading messages that can not use custom terminology --- i18n/en.pot | 4 ++-- .../components/Pages/ViewEvent/epics/viewEvent.epics.ts | 8 ++++---- .../SearchOrgUnitSelector.container.ts | 3 ++- .../SearchOrgUnitSelector.container.ts | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7b7d06d9fa..6a3836b7ae 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T12:44:57.325Z\n" -"PO-Revision-Date: 2026-09-01T12:44:57.325Z\n" +"POT-Creation-Date: 2026-09-01T13:34:18.036Z\n" +"PO-Revision-Date: 2026-09-01T13:34:18.036Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts index 70e2b21e1d..cb746086c1 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts @@ -42,7 +42,7 @@ export const getEventOpeningFromEventListEpic = ( .then((eventContainer: any) => { if (!eventContainer) { return openViewEventPageFailed( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); } const orgUnitLabel = getTermLabel(eventContainer.event.programId, 'orgUnit'); return getCoreOrgUnit({ @@ -67,7 +67,7 @@ export const getEventOpeningFromEventListEpic = ( message || i18n.t('Event could not be loaded'))(details)); return openViewEventPageFailed( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); }), ), ); @@ -86,7 +86,7 @@ export const getEventFromUrlEpic = ( .then((eventContainer: any) => { if (!eventContainer) { return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); } return getCategoriesDataFromEventAsync(eventContainer.event, querySingleResource) .then((categoriesData: any) => eventFromUrlRetrieved(eventContainer, prevProgramId, categoriesData)); @@ -98,7 +98,7 @@ export const getEventFromUrlEpic = ( message || i18n.t('Event could not be loaded'))(details)); return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); }); })); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts index d43148063c..5cf033c7bc 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts @@ -1,4 +1,5 @@ import { connect } from 'react-redux'; +import i18n from '@dhis2/d2-i18n'; import { setOrgUnitScope, setOrgUnit, @@ -25,7 +26,7 @@ const mapStateToProps = (state: ReduxState, props: { searchId: string }) => { treeSearchText: teiSearch.orgUnitsSearchText, treeReady: !teiSearch.orgUnitsLoading, treeKey: teiSearch.orgUnitsSearchText || 'initial', - orgUnitLabel: getTermLabel(programId, 'orgUnit'), + orgUnitLabel: programId ? getTermLabel(programId, 'orgUnit') : i18n.t('organisation unit'), }; }; diff --git a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts index 35e4b017d2..a0cf0b410b 100644 --- a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts +++ b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts @@ -1,4 +1,5 @@ import { connect } from 'react-redux'; +import i18n from '@dhis2/d2-i18n'; import { setOrgUnitScope, setOrgUnit, @@ -24,7 +25,7 @@ const mapStateToProps = (state: any, props: any) => { treeSearchText: teiSearch.orgUnitsSearchText, treeReady: !teiSearch.orgUnitsLoading, treeKey: teiSearch.orgUnitsSearchText || 'initial', - orgUnitLabel: getTermLabel(programId, 'orgUnit'), + orgUnitLabel: programId ? getTermLabel(programId, 'orgUnit') : i18n.t('organisation unit'), }; }; From 7a8e01fcb5b7202728feaad9ca9e786f55f2d6d3 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:58:07 +0000 Subject: [PATCH 050/118] feat: relationship labels singular --- i18n/en.pot | 31 +--------------- .../addRelationshipForNewSingleEvent.epics.ts | 6 +-- ...wEventNewRelationshipWrapper.component.tsx | 37 +++++++++++++------ .../DataEntryEnrollment.component.tsx | 8 +++- ...wEventNewRelationshipWrapper.component.tsx | 37 +++++++++++++------ .../ViewEventRelationships.epics.ts | 6 +-- .../DataEntryEnrollment.component.tsx | 8 +++- .../Relationships/Relationships.component.tsx | 5 ++- 8 files changed, 75 insertions(+), 63 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 6a3836b7ae..57b2e8a9b3 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T13:34:18.036Z\n" -"PO-Revision-Date: 2026-09-01T13:34:18.036Z\n" +"POT-Creation-Date: 2026-09-01T14:58:08.714Z\n" +"PO-Revision-Date: 2026-09-01T14:58:08.714Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -188,13 +188,6 @@ msgstr "" "This is not an event program or the metadata is corrupt. See log for " "details." -msgid "" -"Relationship of type {{relationshipTypeName}} to {{entityName}} already " -"exists" -msgstr "" -"Relationship of type {{relationshipTypeName}} to {{entityName}} already " -"exists" - msgid "Active" msgstr "Active" @@ -231,13 +224,6 @@ msgstr "Switch to row view" msgid "Discard unsaved changes?" msgstr "Discard unsaved changes?" -msgid "" -"Leaving this page will discard the selections you made for a new " -"relationship" -msgstr "" -"Leaving this page will discard the selections you made for a new " -"relationship" - msgid "Yes, discard changes" msgstr "Yes, discard changes" @@ -693,9 +679,6 @@ msgstr "Save {{trackedEntityTypeName}}" msgid "Save {{trackedEntityName}}" msgstr "Save {{trackedEntityName}}" -msgid "Enter details now is not available when creating a relationship" -msgstr "Enter details now is not available when creating a relationship" - msgid "Save new {{trackedEntityTypeName}} and link" msgstr "Save new {{trackedEntityTypeName}} and link" @@ -744,13 +727,6 @@ msgstr "Back" msgid "View changelog" msgstr "View changelog" -msgid "" -"Leaving this page will discard any selections you made for a new " -"relationship" -msgstr "" -"Leaving this page will discard any selections you made for a new " -"relationship" - msgid "Errors" msgstr "Errors" @@ -859,9 +835,6 @@ msgstr "View only - {{message}}" msgid "View only" msgstr "View only" -msgid "Add relationship" -msgstr "Add relationship" - msgid "No results found for " msgstr "No results found for " diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts index c976ceedd3..e7a77e364b 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts @@ -1,7 +1,6 @@ import uuid from 'd2-utilizr/lib/uuid'; import { ofType } from 'redux-observable'; import { map } from 'rxjs/operators'; -import i18n from '@dhis2/d2-i18n'; import { batchActions } from 'redux-batched-actions'; import type { EpicAction, ReduxStore } from 'capture-core-utils/types'; import { getTermLabel } from '../../../../../../metaData'; @@ -99,9 +98,10 @@ export const addRelationshipForNewSingleEventEpic = (action$: EpicAction ( -
-
- {tCustomTerm('New {{eventLabel}} relationship', { eventLabel: getTermLabel(this.props.programId, 'event') })} + renderHeader = () => { + const eventLabel = getTermLabel(this.props.programId, 'event'); + const relationshipLabel = getTermLabel(this.props.programId, 'relationship'); + return ( +
+
+ {tCustomTerm('New {{eventLabel}} {{relationshipLabel}}', { eventLabel, relationshipLabel })} +
-
- ); + ); + }; render() { const { classes, onCancel, programId, ...passOnProps } = this.props; const eventLabel = getTermLabel(programId, 'event'); + const relationshipLabel = getTermLabel(programId, 'relationship'); return (
- {tCustomTerm('Adding relationship to {{eventLabel}}.', { eventLabel })} + + {tCustomTerm('Adding {{relationshipLabel}} to {{eventLabel}}.', { eventLabel, relationshipLabel })} + - {tCustomTerm('Go back to {{eventLabel}} without saving relationship', { eventLabel })} + {tCustomTerm( + 'Go back to {{eventLabel}} without saving {{relationshipLabel}}', + { eventLabel, relationshipLabel }, + )}
this.props.onCancel('relationship')} diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx index 7f76a7bc81..b5dc794ad3 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx @@ -6,6 +6,8 @@ import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../DataEntries'; import type { Props } from './dataEntryEnrollment.types'; import { relatedStageActions } from '../../../../../WidgetRelatedStages'; +import { getTermLabel } from '../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const NewEnrollmentRelationshipPlain = ({ @@ -20,10 +22,14 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + const relationshipLabel = getTermLabel(programId, 'relationship'); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, - disabledMessage: i18n.t('Enter details now is not available when creating a relationship'), + disabledMessage: tCustomTerm( + 'Enter details now is not available when creating a {{relationshipLabel}}', + { relationshipLabel }, + ), }, [relatedStageActions.LINK_EXISTING_RESPONSE]: { hidden: true }, }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx index c46faecc13..ab3e03a759 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx @@ -67,40 +67,53 @@ class ViewEventNewRelationshipWrapperPlain extends React.Component this.setState({ discardDialogOpen: false }); } - renderHeader = () => ( -
-
- {tCustomTerm('New {{eventLabel}} relationship', { eventLabel: getTermLabel(this.props.programId, 'event') })} + renderHeader = () => { + const eventLabel = getTermLabel(this.props.programId, 'event'); + const relationshipLabel = getTermLabel(this.props.programId, 'relationship'); + return ( +
+
+ {tCustomTerm('New {{eventLabel}} {{relationshipLabel}}', { eventLabel, relationshipLabel })} +
-
- ); + ); + }; render() { const { classes, onCancel, programId, ...passOnProps } = this.props; const eventLabel = getTermLabel(programId, 'event'); + const relationshipLabel = getTermLabel(programId, 'relationship'); return (
- {tCustomTerm('Adding relationship to {{eventLabel}}.', { eventLabel })} + + {tCustomTerm('Adding {{relationshipLabel}} to {{eventLabel}}.', { eventLabel, relationshipLabel })} + - {tCustomTerm('Go back to {{eventLabel}} without saving relationship', { eventLabel })} + {tCustomTerm('Go back to {{eventLabel}} without saving {{relationshipLabel}}', { + eventLabel, + relationshipLabel, + })}
r.to.id && r.to.id === clientRelationship.to.id) ) { - const message = i18n.t( - 'Relationship of type {{relationshipTypeName}} to {{entityName}} already exists', + const message = tCustomTerm( + '{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} already exists', { + relationshipLabel: getTermLabel(programId, 'relationship'), entityName: clientRelationship.from.name, relationshipTypeName: clientRelationship.relationshipType.name, }, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx index 07a9bf15a0..db9906633e 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx @@ -6,6 +6,8 @@ import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; import type { Props } from './dataEntryEnrollment.types'; import { relatedStageActions } from '../../../../../../WidgetRelatedStages'; +import { getTermLabel } from '../../../../../../../metaData'; +import { tCustomTerm } from '../../../../../../../utils/tCustomTerm'; const NewEnrollmentRelationshipPlain = ({ @@ -21,10 +23,14 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + const relationshipLabel = getTermLabel(programId, 'relationship'); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, - disabledMessage: i18n.t('Enter details now is not available when creating a relationship'), + disabledMessage: tCustomTerm( + 'Enter details now is not available when creating a {{relationshipLabel}}', + { relationshipLabel }, + ), }, [relatedStageActions.LINK_EXISTING_RESPONSE]: { hidden: true }, }; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index cd39031c63..6e96bfd1da 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { cx } from '@emotion/css'; -import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { IconButton } from 'capture-ui'; import { IconDelete16, Button, colors } from '@dhis2/ui'; @@ -172,8 +171,10 @@ class RelationshipsPlain extends React.Component { writableRelationshipTypes, relationshipsRef, smallMainButton, + programId, } = this.props; const canCreate = !readOnly && writableRelationshipTypes.length > 0; + const relationshipLabel = getTermLabel(programId, 'relationship'); return (
@@ -188,7 +189,7 @@ class RelationshipsPlain extends React.Component { dataTest="add-relationship-button" secondary > - {i18n.t('Add relationship')} + {tCustomTerm('Add {{relationshipLabel}}', { relationshipLabel })}
From 7a8a71330b3a9e264265900ad4ad1b6a13aab44e Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:58:15 +0000 Subject: [PATCH 051/118] fix: sonar qube --- i18n/en.pot | 4 ++-- .../StageEventHeader/StageEventHeader.component.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 57b2e8a9b3..4fc2a17ba1 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T14:58:08.714Z\n" -"PO-Revision-Date: 2026-09-01T14:58:08.714Z\n" +"POT-Creation-Date: 2026-09-01T14:58:16.773Z\n" +"PO-Revision-Date: 2026-09-01T14:58:16.773Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx index e2f6034354..a20add4340 100644 --- a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx +++ b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx @@ -25,7 +25,7 @@ type Props = PlainProps & WithStyles; const StageEventHeaderPlain = ({ icon, title, events, classes }: Props) => { const eventLabel = useTermLabel('event'); const eventsLabel = useTermLabel('event', { plural: true }); - return (<> + return (
{ icon && ( @@ -52,7 +52,7 @@ const StageEventHeaderPlain = ({ icon, title, events, classes }: Props) => { }
- ); + ); }; export const StageEventHeader = withStyles( From 99d95e8224c82c1f9cb12a8176a3ef7090fc1eba Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:24:23 +0000 Subject: [PATCH 052/118] feat: note labels singular --- i18n/en.pot | 19 ++---------- .../DataEntry/DataEntry.component.tsx | 3 +- .../DataEntry/DataEntry.container.ts | 1 + .../note.validatorContainersGetter.ts | 5 ++-- .../helpers/getOpenDataEntryActions.ts | 9 ++++-- .../components/Notes/Notes.component.tsx | 7 +++-- .../NotesSection/NotesSection.component.tsx | 1 + .../DataEntry/DataEntry.component.tsx | 3 +- .../DataEntry/DataEntry.container.tsx | 1 + .../note.validatorContainersGetter.ts | 5 ++-- .../helpers/getOpenDataEntryActions.ts | 9 ++++-- .../WidgetEnrollmentNote.component.tsx | 7 ++++- .../WidgetEventNote.component.tsx | 4 ++- .../WidgetEventSchedule.component.tsx | 7 ++++- .../WidgetNote/NoteSection/NoteSection.tsx | 4 ++- .../NoteSection/NoteSection.types.ts | 1 + .../components/WidgetNote/WidgetNote.types.ts | 1 + .../feedback.reducerDescriptionGetter.ts | 30 +++++++++++++++---- 18 files changed, 78 insertions(+), 39 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 4fc2a17ba1..a183c35e62 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T14:58:16.773Z\n" -"PO-Revision-Date: 2026-09-01T14:58:16.773Z\n" +"POT-Creation-Date: 2026-09-01T15:24:24.169Z\n" +"PO-Revision-Date: 2026-09-01T15:24:24.169Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -561,12 +561,6 @@ msgstr "Rows per page" msgid "Program doesn't exist" msgstr "Program doesn't exist" -msgid "Add note" -msgstr "Add note" - -msgid "Write note" -msgstr "Write note" - msgid "{{fieldName}} was blanked out and hidden by your last action" msgstr "{{fieldName}} was blanked out and hidden by your last action" @@ -1159,9 +1153,6 @@ msgstr "Feedback" msgid "Indicators" msgstr "Indicators" -msgid "Save note" -msgstr "Save note" - msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" @@ -1599,12 +1590,6 @@ msgstr "Error editing the event, the changes made were not saved" msgid "Error updating the Assignee" msgstr "Error updating the Assignee" -msgid "Could not save enrollment note" -msgstr "Could not save enrollment note" - -msgid "Could not save event note" -msgstr "Could not save event note" - msgid "There was an error fetching metadata" msgstr "There was an error fetching metadata" diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index 806d92803e..a079328b20 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -406,9 +406,10 @@ const buildNotesSettingsFn = () => { onAddNote: props.onAddNote, id: 'notes', dataEntryId: props.id, + noteLabel: props.noteLabel, }), getPropName: () => 'note', - getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel), + getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel, props.noteLabel), getMeta: () => ({ placement: placements.BOTTOM, section: dataEntrySectionNames.NOTES, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts index 5e49de8368..b0b4bfd023 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts @@ -30,6 +30,7 @@ import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; const customLabels = { orgUnitLabel: { key: 'orgUnit' }, eventLabel: { key: 'event' }, + noteLabel: { key: 'note' }, } as const; const makeMapStateToProps = () => { diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts index 7e0f8a5b08..25a79c0260 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts @@ -2,11 +2,12 @@ import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; const validateNote = (value?: string) => !value; -export const getNoteValidatorContainers = (eventLabel: string) => [ +export const getNoteValidatorContainers = (eventLabel: string, noteLabel: string) => [ { validator: validateNote, - errorMessage: tCustomTerm('Please add or cancel the note before saving the {{eventLabel}}', { + errorMessage: tCustomTerm('Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}', { eventLabel, + noteLabel, }), }, ]; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts index e5f95aef47..0deb99054f 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts @@ -10,7 +10,11 @@ import type { ProgramCategory } from '../../../../../WidgetEventSchedule/Categor import type { DataEntryPropToInclude } from '../../../../../DataEntry/actions/dataEntryLoad.utils'; import { getTermLabel } from '../../../../../../metaData/helpers/customLabels'; -const buildDataEntryPropsToInclude = (orgUnitLabel: string, eventLabel: string): Array => [ +const buildDataEntryPropsToInclude = ( + orgUnitLabel: string, + eventLabel: string, + noteLabel: string, +): Array => [ { id: 'occurredAt', type: 'DATE', @@ -29,7 +33,7 @@ const buildDataEntryPropsToInclude = (orgUnitLabel: string, eventLabel: string): { id: 'note', type: 'TEXT', - validatorContainers: getNoteValidatorContainers(eventLabel), + validatorContainers: getNoteValidatorContainers(eventLabel, noteLabel), clientIgnore: true, }, { @@ -64,6 +68,7 @@ export const getOpenDataEntryActions = ( const dataEntryPropsToInclude = buildDataEntryPropsToInclude( getTermLabel(programId, 'orgUnit'), getTermLabel(programId, 'event'), + getTermLabel(programId, 'note'), ); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.tsx b/src/core_modules/capture-core/components/Notes/Notes.component.tsx index b5b9d305d8..1690fb7bba 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.tsx +++ b/src/core_modules/capture-core/components/Notes/Notes.component.tsx @@ -10,6 +10,7 @@ import { withFocusSaver } from 'capture-ui'; import { TextField } from '../FormFields/New'; import { convertClientToList } from '../../converters'; import { dataElementTypes } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import type { Note } from './notes.types'; const FocusTextField = withFocusSaver()(TextField); @@ -69,6 +70,7 @@ type Props = { value: string | null; readOnly?: boolean; smallMainButton?: boolean; + noteLabel: string; }; type NotesProps = Props & WithStyles; @@ -80,6 +82,7 @@ const NotesPlain = ({ value: propValue, readOnly = false, smallMainButton, + noteLabel, classes, }: NotesProps) => { const [addIsOpen, setAddIsOpen] = useState(false); @@ -134,7 +137,7 @@ const NotesPlain = ({ className={classes.addNoteContainer} small > - {i18n.t('Add note')} + {tCustomTerm('Add {{noteLabel}}', { noteLabel })}
); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx index f9e54b2242..05789b7468 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx @@ -73,6 +73,7 @@ class NotesSectionPlain extends React.Component { onBlur: this.props.onUpdateNoteField, value: fieldValue, smallMainButton: true, + noteLabel: getTermLabel(programId, 'note'), })} ); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx index 0ad4c732a5..fee540d19f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx @@ -312,9 +312,10 @@ const buildNotesSettingsFn = () => { onAddNote: props.onAddNote, id: 'notes', dataEntryId: props.id, + noteLabel: props.noteLabel, }), getPropName: () => 'note', - getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel), + getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel, props.noteLabel), getMeta: () => ({ placement: placements.BOTTOM, section: dataEntrySectionNames.NOTES, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx index 0fb8d17bb0..37a7ef8421 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx @@ -20,6 +20,7 @@ import { withCustomLabels } from '../../../HOC/withCustomLabels'; const customLabels = { orgUnitLabel: { key: 'orgUnit' }, eventLabel: { key: 'event' }, + noteLabel: { key: 'note' }, } as const; const WrappedDataEntryComponent = withCustomLabels(customLabels)(DataEntryComponent); diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts index 84a503f322..61a0b6c041 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/note.validatorContainersGetter.ts @@ -2,11 +2,12 @@ import { tCustomTerm } from '../../../../utils/tCustomTerm'; const validateNote = (value?: string | null) => !value; -export const getNoteValidatorContainers = (eventLabel: string) => [ +export const getNoteValidatorContainers = (eventLabel: string, noteLabel: string) => [ { validator: validateNote, - errorMessage: tCustomTerm('Please add or cancel the note before saving the {{eventLabel}}', { + errorMessage: tCustomTerm('Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}', { eventLabel, + noteLabel, }), }, ]; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts index da555d10c9..cb846c4798 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts @@ -7,7 +7,11 @@ import { getCategoryOptionsValidatorContainers } from '../fieldValidators/catego import type { DataEntryPropToInclude } from '../../../DataEntry/actions/dataEntryLoad.utils'; import { getTermLabel } from '../../../../metaData/helpers/customLabels'; -const buildDataEntryPropsToInclude = (orgUnitLabel: string, eventLabel: string): Array => [ +const buildDataEntryPropsToInclude = ( + orgUnitLabel: string, + eventLabel: string, + noteLabel: string, +): Array => [ { id: 'occurredAt', type: 'DATE', @@ -31,7 +35,7 @@ const buildDataEntryPropsToInclude = (orgUnitLabel: string, eventLabel: string): { id: 'note', type: 'TEXT', - validatorContainers: getNoteValidatorContainers(eventLabel), + validatorContainers: getNoteValidatorContainers(eventLabel, noteLabel), clientIgnore: true, }, { @@ -56,6 +60,7 @@ export const getOpenDataEntryActions = const dataEntryPropsToInclude = buildDataEntryPropsToInclude( getTermLabel(programId, 'orgUnit'), getTermLabel(programId, 'event'), + getTermLabel(programId, 'note'), ); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx index 78c03b4ce3..dbcb959714 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -19,6 +19,7 @@ export const WidgetEnrollmentNote = () => { showWidgetBadge, } = useEnrollmentAccessContext(); const enrollmentLabel = useTermLabel('enrollment'); + const noteLabel = useTermLabel('note'); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); @@ -28,8 +29,12 @@ export const WidgetEnrollmentNote = () => {
{ dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue, programId)); @@ -27,8 +28,9 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId, programId }: Props)
{enableUserAssignment && ( diff --git a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx index 33cb4f4f7b..77fec2387f 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx @@ -9,6 +9,7 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { TextField } from '../../FormFields/New'; import { convertClientToList } from '../../../converters'; import { dataElementTypes } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import type { OwnProps, NoteType } from './NoteSection.types'; const FocusTextField = withFocusSaver()(TextField); @@ -71,6 +72,7 @@ const NoteSectionPlain = ({ notes, handleAddNote, readOnly, + noteLabel, classes, }: Props) => { const [isEditing, setEditing] = useState(false); @@ -158,7 +160,7 @@ const NoteSectionPlain = ({ primary small > - {i18n.t('Save note')} + {tCustomTerm('Save {{noteLabel}}', { noteLabel })}
)} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx index aacc936b0d..a22b7998ca 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -10,6 +10,7 @@ type Props = { repeatable?: boolean; preventAddingEventActionInEffect?: boolean; eventName: string; + stageId: string; }; export const StageCreateNewButton = ({ @@ -18,8 +19,9 @@ export const StageCreateNewButton = ({ repeatable, preventAddingEventActionInEffect, eventName, + stageId, }: Props) => { - const programStageLabel = useTermLabel('programStage'); + const programStageLabel = useTermLabel('programStage', { stageId }); const eventLabel = useTermLabel('event'); const eventsLabel = useTermLabel('event', { plural: true }); const { isDisabled, tooltipContent } = useMemo(() => { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index 57f0bc1da9..28db61a7f8 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx @@ -271,6 +271,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { preventAddingEventActionInEffect={hiddenProgramStage} repeatable={repeatable} eventName={eventName} + stageId={stageId} />
) : null); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index 910e5d4275..0f7c02699a 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -94,7 +94,7 @@ const getLastUpdatedAt = (events: Array, fromServerDate: (da }; export const StageOverviewPlain = ({ - title, icon, description, events, stageWriteAccess = true, classes, + title, icon, description, events, stageWriteAccess = true, stageId, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); const { anyStageWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); @@ -168,6 +168,7 @@ export const StageOverviewPlain = ({ {showStageBadge && ( )}
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts index 8889520a25..80eb891438 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts @@ -7,4 +7,5 @@ export type Props = { icon?: Icon; description?: string | null; stageWriteAccess?: boolean; + stageId: string; }; From a18315be57c4692935f9c47564fec7097d75c99c Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:08:56 +0000 Subject: [PATCH 055/118] fix: update getTermLabel to correctly pass programId from options --- i18n/en.pot | 4 ++-- .../capture-core/metaData/helpers/customLabels.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 510dfd455f..69bf7d9e86 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-01T10:32:13.670Z\n" -"PO-Revision-Date: 2026-09-01T10:32:13.670Z\n" +"POT-Creation-Date: 2026-09-02T08:08:57.448Z\n" +"PO-Revision-Date: 2026-09-02T08:08:57.448Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts index caf195f551..6aabb86c77 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -98,10 +98,9 @@ const resolveTerm = ( }; export const getTermLabel = ( - programId: string | undefined, key: CustomLabelKey, - options: TermLabelOptions = {}, -): string => resolveTerm(programId, key, options); + options: TermLabelOptions & { programId: string }, +): string => resolveTerm(options.programId, key, options); export const useTermLabel = ( key: CustomLabelKey, From 919a9b2f07f18192ad24ea35dc0b5cd89e47a193 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:20:49 +0000 Subject: [PATCH 056/118] fix: update getTermLabel calls to use options object for programId --- i18n/en.pot | 4 ++-- .../EnrollmentDataEntry.component.tsx | 2 +- ...llmentWithFirstStageDataEntry.component.tsx | 2 +- .../addRelationshipForNewSingleEvent.epics.ts | 4 ++-- .../helpers/getOpenDataEntryActions.ts | 6 +++--- ...ewEventNewRelationshipWrapper.component.tsx | 8 ++++---- .../DataEntry/withAskToCreateNew.tsx | 2 +- .../DataEntryWidgetOutput.container.ts | 2 +- .../LockedSelector/LockedSelector.epics.ts | 4 ++-- .../Enrollment/epics/enrollmentPage.epics.ts | 2 +- .../Enrollment/epics/fetchEnrollment.epics.ts | 4 ++-- .../DataEntryEnrollment.component.tsx | 2 +- ...ewEventNewRelationshipWrapper.component.tsx | 8 ++++---- .../ViewEventRelationships.epics.ts | 4 ++-- .../NotesSection/NotesSection.component.tsx | 4 ++-- .../RelationshipsSection.component.tsx | 2 +- .../ViewEventComponent/ViewEvent.container.ts | 2 +- .../Pages/ViewEvent/epics/editEvent.epics.ts | 2 +- .../Pages/ViewEvent/epics/viewEvent.epics.ts | 4 ++-- .../DataEntryEnrollment.component.tsx | 2 +- .../SearchOrgUnitSelector.container.ts | 2 +- .../Relationships/Relationships.component.tsx | 4 ++-- .../SearchOrgUnitSelector.container.ts | 2 +- .../DataEntry/epics/dataEntryRules.epics.ts | 2 +- .../helpers/getOpenDataEntryActions.ts | 6 +++--- .../DataEntry/editEventDataEntry.actions.ts | 4 ++-- .../epics/editEventDataEntry.epics.ts | 2 +- .../DataEntry/withDeleteButton.tsx | 2 +- .../viewEventDataEntry.actions.ts | 4 ++-- .../Actions/CompleteAction/CompleteAction.tsx | 2 +- .../feedback.reducerDescriptionGetter.ts | 18 +++++++++--------- 31 files changed, 59 insertions(+), 59 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index de45fa71ed..3398fbfe0e 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T08:12:41.268Z\n" -"PO-Revision-Date: 2026-09-02T08:12:41.268Z\n" +"POT-Creation-Date: 2026-09-02T08:20:50.233Z\n" +"PO-Revision-Date: 2026-09-02T08:20:50.233Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx index 15dcfb016f..04242353c2 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx @@ -368,7 +368,7 @@ class FinalEnrollmentDataEntry extends React.Component { const dataEntrySections = { [sectionKeysForEnrollmentDataEntry.ENROLLMENT]: { placement: placements.TOP, - name: getTermLabel(programId, 'enrollment'), + name: getTermLabel('enrollment', { programId }), }, [AOCsectionKey]: { placement: placements.BOTTOM, diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx index fabbf82396..daf126fd64 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx @@ -180,7 +180,7 @@ const getCompleteFieldSettingsFn = () => { getComponent: () => completeComponent, getComponentProps: (props: any) => createComponentProps(props, { label: tCustomTerm('Complete {{eventLabel}}', { - eventLabel: getTermLabel(props.programId, 'event'), + eventLabel: getTermLabel('event', { programId: props.programId }), }), id: 'complete', }), diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts index e7a77e364b..8c457906bd 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts @@ -82,7 +82,7 @@ export const addRelationshipForNewSingleEventEpic = (action$: EpicAction ({ diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx index 5f5f45b694..597c14a426 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx @@ -83,8 +83,8 @@ class NewEventNewRelationshipWrapper extends React.Component { - const eventLabel = getTermLabel(this.props.programId, 'event'); - const relationshipLabel = getTermLabel(this.props.programId, 'relationship'); + const eventLabel = getTermLabel('event', { programId: this.props.programId }); + const relationshipLabel = getTermLabel('relationship', { programId: this.props.programId }); return (
diff --git a/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx b/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx index e920e27a93..309e8573db 100644 --- a/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx +++ b/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx @@ -54,7 +54,7 @@ const askToCreateNewComponent = (InnerComponent: React.ComponentType) => if (!this.state.isOpen) { return null; } - const eventLabel = getTermLabel(this.props.programId, 'event'); + const eventLabel = getTermLabel('event', { programId: this.props.programId }); return ( { const { dataEntries } = state; const ready = !!dataEntries[dataEntryId]; const dataEntryKey = ready ? getDataEntryKey(dataEntryId, state.dataEntries[dataEntryId].itemId) : null; - const enrollmentLabel = getTermLabel(selectedScopeId, 'enrollment'); + const enrollmentLabel = getTermLabel('enrollment', { programId: selectedScopeId }); return { ready, diff --git a/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts b/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts index d949129b33..d9258ebb1c 100644 --- a/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts +++ b/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts @@ -30,7 +30,7 @@ export const getOrgUnitDataBasedOnUrlUpdateEpic = (action$: EpicAction, sto if (organisationUnits[orgUnitId]) { return of(completeUrlUpdate()); } - const orgUnitLabel = getTermLabel(programId, 'orgUnit'); + const orgUnitLabel = getTermLabel('orgUnit', { programId }); return of(startLoading(), getCoreOrgUnit({ orgUnitId, onSuccess: setCurrentOrgUnitBasedOnUrl, @@ -68,7 +68,7 @@ export const validateSelectionsBasedOnUrlUpdateEpic = (action$: EpicAction) } if (orgUnitId && !program.organisationUnits[orgUnitId]) { - const orgUnitLabel = getTermLabel(programId, 'orgUnit'); + const orgUnitLabel = getTermLabel('orgUnit', { programId }); return invalidSelectionsFromUrl( tCustomTerm('Selected program is invalid for selected {{orgUnitLabel}}', { orgUnitLabel }), ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts b/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts index d0943bceb1..6000ca3772 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts @@ -124,7 +124,7 @@ export const enrollmentIdErrorEpic = (action$: any, store: any) => ofType(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_ERROR), map(({ payload: { enrollmentId } }) => { const { programId } = store.value.enrollmentPage; - const enrollmentLabel = getTermLabel(programId, 'enrollment'); + const enrollmentLabel = getTermLabel('enrollment', { programId }); return showErrorViewOnEnrollmentPage({ error: tCustomTerm('{{enrollmentLabel}} with id "{{enrollmentId}}" does not exist', { enrollmentLabel, enrollmentId }), diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts b/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts index d6c3cc0ed1..e9e0009ce1 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts @@ -120,7 +120,7 @@ const handleErrorsFromNewerBackends = ({ querySingleResource, })); } - const enrollmentsLabel = getTermLabel(programId, 'enrollment', { plural: true }); + const enrollmentsLabel = getTermLabel('enrollment', { programId, plural: true }); const errorMessage = tCustomTerm( 'An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', { enrollmentsLabel }, @@ -141,7 +141,7 @@ const handleErrorsFromOlderBackends = (error: any, programId: string) => { return fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.NO_ACCESS }); } } - const enrollmentsLabel = getTermLabel(programId, 'enrollment', { plural: true }); + const enrollmentsLabel = getTermLabel('enrollment', { programId, plural: true }); const errorMessage = tCustomTerm( 'An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', { enrollmentsLabel }, diff --git a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx index b5dc794ad3..5121a5cccf 100644 --- a/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/Pages/NewRelationship/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx @@ -22,7 +22,7 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; - const relationshipLabel = getTermLabel(programId, 'relationship'); + const relationshipLabel = getTermLabel('relationship', { programId }); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx index ab3e03a759..8e18af2915 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx @@ -68,8 +68,8 @@ class ViewEventNewRelationshipWrapperPlain extends React.Component } renderHeader = () => { - const eventLabel = getTermLabel(this.props.programId, 'event'); - const relationshipLabel = getTermLabel(this.props.programId, 'relationship'); + const eventLabel = getTermLabel('event', { programId: this.props.programId }); + const relationshipLabel = getTermLabel('relationship', { programId: this.props.programId }); return (
render() { const { classes, onCancel, programId, ...passOnProps } = this.props; - const eventLabel = getTermLabel(programId, 'event'); - const relationshipLabel = getTermLabel(programId, 'relationship'); + const eventLabel = getTermLabel('event', { programId }); + const relationshipLabel = getTermLabel('relationship', { programId }); return (
diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts index a3130312c1..a9c6f8177b 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts @@ -85,7 +85,7 @@ export const addRelationshipForViewEventEpic = (action$: any, store: any) => clientId: relationshipClientId, from: { id: eventId, - name: tCustomTerm('This {{eventLabel}}', { eventLabel: getTermLabel(programId, 'event') }), + name: tCustomTerm('This {{eventLabel}}', { eventLabel: getTermLabel('event', { programId }) }), type: 'PROGRAM_STAGE_INSTANCE', }, to: { @@ -104,7 +104,7 @@ export const addRelationshipForViewEventEpic = (action$: any, store: any) => const message = tCustomTerm( '{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} already exists', { - relationshipLabel: getTermLabel(programId, 'relationship'), + relationshipLabel: getTermLabel('relationship', { programId }), entityName: clientRelationship.from.name, relationshipTypeName: clientRelationship.relationshipType.name, }, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx index 05789b7468..13234e3851 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx @@ -61,7 +61,7 @@ class NotesSectionPlain extends React.Component {
{tCustomTerm( "This {{eventLabel}} doesn't have any notes", - { eventLabel: getTermLabel(programId, 'event') }, + { eventLabel: getTermLabel('event', { programId }) }, )}
)} @@ -73,7 +73,7 @@ class NotesSectionPlain extends React.Component { onBlur: this.props.onUpdateNoteField, value: fieldValue, smallMainButton: true, - noteLabel: getTermLabel(programId, 'note'), + noteLabel: getTermLabel('note', { programId }), })} ); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx index c98b310f95..f6eef80847 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx @@ -96,7 +96,7 @@ class RelationshipsSectionPlain extends React.Component {
{tCustomTerm( "This {{eventLabel}} doesn't have any relationships", - { eventLabel: getTermLabel(programId, 'event') }, + { eventLabel: getTermLabel('event', { programId }) }, )}
)} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts index 9ccddb8f3f..82e569ca16 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts @@ -32,7 +32,7 @@ const makeMapStateToProps = () => { : getDataEntryKey(dataEntryIds.SINGLE_EVENT, dataEntryKeys.VIEW); const isUserInteractionInProgress = dataEntryHasChanges(state, currentDataEntryKey); const programId = state.currentSelections.programId; - const eventLabel = programId ? getTermLabel(programId, 'event') : undefined; + const eventLabel = programId ? getTermLabel('event', { programId }) : undefined; return { programStage: programStageSelector(state), eventAccess: eventAccessSelector(state), diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts index f050abdd95..0527e27b84 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts @@ -23,7 +23,7 @@ export const getEventFromUrlEpic = ( const eventId = action.payload.eventId; const orgUnit = action.payload.orgUnit; const prevProgramId = store.value.currentSelections.programId; - const eventLabel = getTermLabel(prevProgramId, 'event'); + const eventLabel = getTermLabel('event', { programId: prevProgramId }); return getEvent(eventId, absoluteApiPath, querySingleResource) .then((eventContainer: any) => { if (!eventContainer) { diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts index cb746086c1..b13a598037 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts @@ -44,7 +44,7 @@ export const getEventOpeningFromEventListEpic = ( return openViewEventPageFailed( i18n.t('Could not load the requested data. It may not exist or you may not have access.')); } - const orgUnitLabel = getTermLabel(eventContainer.event.programId, 'orgUnit'); + const orgUnitLabel = getTermLabel('orgUnit', { programId: eventContainer.event.programId }); return getCoreOrgUnit({ orgUnitId: eventContainer.event.orgUnitId, onSuccess: (orgUnit: CoreOrgUnit) => startOpenEventForView(eventContainer, orgUnit), @@ -107,7 +107,7 @@ export const getOrgUnitOnUrlUpdateEpic = (action$: any) => ofType(viewEventActionTypes.EVENT_FROM_URL_RETRIEVED), map((action: any) => { const eventContainer = action.payload.eventContainer; - const orgUnitLabel = getTermLabel(eventContainer.event.programId, 'orgUnit'); + const orgUnitLabel = getTermLabel('orgUnit', { programId: eventContainer.event.programId }); return getCoreOrgUnit({ orgUnitId: eventContainer.event.orgUnitId, onSuccess: (orgUnit: CoreOrgUnit) => orgUnitRetrievedOnUrlUpdate(orgUnit, eventContainer), diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx index db9906633e..3129ab4956 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx @@ -23,7 +23,7 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; - const relationshipLabel = getTermLabel(programId, 'relationship'); + const relationshipLabel = getTermLabel('relationship', { programId }); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts index 5cf033c7bc..b2073f943e 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts @@ -26,7 +26,7 @@ const mapStateToProps = (state: ReduxState, props: { searchId: string }) => { treeSearchText: teiSearch.orgUnitsSearchText, treeReady: !teiSearch.orgUnitsLoading, treeKey: teiSearch.orgUnitsSearchText || 'initial', - orgUnitLabel: programId ? getTermLabel(programId, 'orgUnit') : i18n.t('organisation unit'), + orgUnitLabel: programId ? getTermLabel('orgUnit', { programId }) : i18n.t('organisation unit'), }; }; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index 6e96bfd1da..2dd0866912 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -65,7 +65,7 @@ const styles: Readonly = (theme: any) => ({ }); const getFromNames = (programId: string) => ({ - PROGRAM_STAGE_INSTANCE: tCustomTerm('This {{eventLabel}}', { eventLabel: getTermLabel(programId, 'event') }), + PROGRAM_STAGE_INSTANCE: tCustomTerm('This {{eventLabel}}', { eventLabel: getTermLabel('event', { programId }) }), }); type PlainProps = { @@ -174,7 +174,7 @@ class RelationshipsPlain extends React.Component { programId, } = this.props; const canCreate = !readOnly && writableRelationshipTypes.length > 0; - const relationshipLabel = getTermLabel(programId, 'relationship'); + const relationshipLabel = getTermLabel('relationship', { programId }); return (
diff --git a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts index a0cf0b410b..72848a8b1f 100644 --- a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts +++ b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.container.ts @@ -25,7 +25,7 @@ const mapStateToProps = (state: any, props: any) => { treeSearchText: teiSearch.orgUnitsSearchText, treeReady: !teiSearch.orgUnitsLoading, treeKey: teiSearch.orgUnitsSearchText || 'initial', - orgUnitLabel: programId ? getTermLabel(programId, 'orgUnit') : i18n.t('organisation unit'), + orgUnitLabel: programId ? getTermLabel('orgUnit', { programId }) : i18n.t('organisation unit'), }; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts index 67cc4855ee..ee229eb5d5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/epics/dataEntryRules.epics.ts @@ -50,7 +50,7 @@ const runRulesForNewEvent = async ({ const stage = program.getStage(stageId); if (!stage) { throw Error(tCustomTerm('{{programStageLabel}} not found', { - programStageLabel: getTermLabel(programId, 'programStage', { stageId }), + programStageLabel: getTermLabel('programStage', { programId, stageId }), })); } diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts index cb846c4798..963861c3c7 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts @@ -58,9 +58,9 @@ export const getOpenDataEntryActions = : undefined, }; const dataEntryPropsToInclude = buildDataEntryPropsToInclude( - getTermLabel(programId, 'orgUnit'), - getTermLabel(programId, 'event'), - getTermLabel(programId, 'note'), + getTermLabel('orgUnit', { programId }), + getTermLabel('event', { programId }), + getTermLabel('note', { programId }), ); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts index 0ac87af2fe..e718843195 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/editEventDataEntry.actions.ts @@ -119,7 +119,7 @@ export const openEventForEditInDataEntry = ({ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(getTermLabel(program.id, 'orgUnit')), + validatorContainers: getOrgUnitValidatorContainers(getTermLabel('orgUnit', { programId: program.id })), }, { clientId: 'geometry', @@ -166,7 +166,7 @@ export const openEventForEditInDataEntry = ({ const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { throw Error(tCustomTerm('{{programStageLabel}} not found in rules execution', { - programStageLabel: getTermLabel(program.id, 'programStage'), + programStageLabel: getTermLabel('programStage', { programId: program.id }), })); } // TODO: Add attributeValues & enrollmentData diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts index 255edb572b..65c317cce7 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts @@ -57,7 +57,7 @@ const runRulesForEditSingleEvent = async ({ if (!stage) { throw Error(tCustomTerm('{{programStageLabel}} not found in rules execution', { - programStageLabel: getTermLabel(programId, 'programStage'), + programStageLabel: getTermLabel('programStage', { programId }), })); } diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx index a6c1aa8a87..949db875fe 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx @@ -20,7 +20,7 @@ const getDeleteButton = (InnerComponent: React.ComponentType) => } renderDeleteButton = (hasDeleteButton?: boolean) => { - const eventLabel = getTermLabel(this.props.programId, 'event'); + const eventLabel = getTermLabel('event', { programId: this.props.programId }); return ( hasDeleteButton ? (
diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx index e80faf3656..c5c43c6fc9 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx @@ -51,7 +51,7 @@ const Schedule = ({ programStage, canAddNewEventToStage, }) => { - const eventLabel = useTermLabel('event'); + const eventLabel = useTermLabel('event', { stageId: programStage?.id }); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.SCHEDULE_IN_ORG] || {}; if (hidden) { @@ -96,7 +96,7 @@ const EnterData = ({ programStage, canAddNewEventToStage, }) => { - const eventLabel = useTermLabel('event'); + const eventLabel = useTermLabel('event', { stageId: programStage?.id }); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.ENTER_DATA] || {}; if (hidden) { @@ -141,8 +141,8 @@ const LinkExistingResponse = ({ updateSelectedAction, programStage, }) => { - const eventLabel = useTermLabel('event'); - const eventsLabel = useTermLabel('event', { plural: true }); + const eventLabel = useTermLabel('event', { stageId: programStage?.id }); + const eventsLabel = useTermLabel('event', { stageId: programStage?.id, plural: true }); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.LINK_EXISTING_RESPONSE] || {}; if (hidden) { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx index 938689d4a9..99ed57d0fc 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx @@ -26,7 +26,10 @@ export const DeleteActionModal = ({ onDeleteEvent, onRollbackDeleteEvent, }: Props) => { - const eventLabel = useTermLabel('event'); + const eventLabel = useTermLabel('event', { + programId: eventDetails.program, + stageId: eventDetails.programStage, + }); const { show: showError } = useAlert( ({ message }) => message, { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx index 0bdb8c69df..7febfb2bc9 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx @@ -30,7 +30,10 @@ export const SkipAction = ({ onUpdateEventStatus, }: Props) => { const dataEngine = useDataEngine(); - const eventLabel = useTermLabel('event'); + const eventLabel = useTermLabel('event', { + programId: eventDetails.program, + stageId: eventDetails.programStage, + }); const { show: showError } = useAlert( ({ message }) => message, { critical: true }, diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx index 3e809c71b6..c44ffb1a6e 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx @@ -23,10 +23,11 @@ export const UnlinkAndDeleteModal = ({ relationshipId, onDeleteEvent, onDeleteEventRelationship, + stageId, }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); - const eventLabel = useTermLabel('event'); + const eventLabel = useTermLabel('event', { stageId }); const { show: showErrorAlert } = useAlert( tCustomTerm('An error occurred while unlinking and deleting the {{eventLabel}}.', { eventLabel }), { critical: true }, diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts index 6e324de160..33d72f5998 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts @@ -5,4 +5,5 @@ export type Props = { relationshipId: string; onDeleteEvent?: (eventId: string) => void; onDeleteEventRelationship?: (relationshipId: string) => void; + stageId?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx index 41fea724c2..4dd167e4d2 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx @@ -21,11 +21,12 @@ export const UnlinkModal = ({ relationshipId, originEventId, onDeleteEventRelationship, + stageId, }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); - const eventLabel = useTermLabel('event'); - const eventsLabel = useTermLabel('event', { plural: true }); + const eventLabel = useTermLabel('event', { stageId }); + const eventsLabel = useTermLabel('event', { stageId, plural: true }); const { show: showErrorAlert } = useAlert( tCustomTerm('An error occurred while unlinking and deleting the {{eventLabel}}.', { eventLabel }), { critical: true }, diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts index 144dde66c9..2b97ac46e4 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts @@ -4,4 +4,5 @@ export type Props = { originEventId: string; onDeleteEvent?: (eventId: string) => void; onDeleteEventRelationship?: (relationshipId: string) => void; + stageId?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx index f7eed0eba5..f9f8fae176 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx @@ -113,6 +113,7 @@ export const OverflowMenuComponent = ({ relationshipId={relationshipId} originEventId={originEventId} onDeleteEventRelationship={onDeleteEventRelationship} + stageId={linkedEvent?.programStage} /> )} {isUnlinkAndDeleteModalOpen && ( @@ -123,6 +124,7 @@ export const OverflowMenuComponent = ({ relationshipId={relationshipId} onDeleteEvent={onDeleteEvent} onDeleteEventRelationship={onDeleteEventRelationship} + stageId={linkedEvent?.programStage} /> )} From e6385de8c60dd080a5f416ce857d7a00c33e0559 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:20:38 +0000 Subject: [PATCH 061/118] fix: remove unused displayTrackedEntityTypesLabel from CachedTrackedEntityType --- i18n/en.pot | 4 ++-- .../capture-core/storageControllers/types/cache.types.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index b67e0603f6..3f9d3ddeb6 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T16:03:11.781Z\n" -"PO-Revision-Date: 2026-09-02T16:03:11.781Z\n" +"POT-Creation-Date: 2026-09-02T16:20:40.240Z\n" +"PO-Revision-Date: 2026-09-02T16:20:40.240Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/storageControllers/types/cache.types.ts b/src/core_modules/capture-core/storageControllers/types/cache.types.ts index 771a55c14b..58cd309418 100644 --- a/src/core_modules/capture-core/storageControllers/types/cache.types.ts +++ b/src/core_modules/capture-core/storageControllers/types/cache.types.ts @@ -184,7 +184,6 @@ export type CachedTrackedEntityType = { id: string, access: Access, displayName: string, - displayTrackedEntityTypesLabel?: string | null, trackedEntityTypeAttributes?: Array | null, translations: Array, minAttributesRequiredToSearch: number, From 426a0f4f9c88cbae4e2db9950fbf1dfec53381b2 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:31:50 +0000 Subject: [PATCH 062/118] fix: add missing program id --- i18n/en.pot | 4 ++-- .../StageEventHeader/StageEventHeader.component.tsx | 6 +++--- .../StageEventHeader/StageEventHeader.types.ts | 2 ++ .../StageEvent/StageEventList/StageEventList.component.tsx | 2 ++ .../RelatedStagesActions/RelatedStagesActions.container.tsx | 2 +- .../WidgetTwoEventWorkspace.component.tsx | 5 ++++- .../EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx | 5 +++-- .../EventWorkingListsRowMenuSetup.component.tsx | 1 + .../useDefaultColumnConfiguration/useDefaultColumnConfig.ts | 2 +- .../Setup/hooks/useDefaultColumnConfig.ts | 2 +- .../TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts | 6 +++--- 11 files changed, 23 insertions(+), 14 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3f9d3ddeb6..fb99f61965 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T16:20:40.240Z\n" -"PO-Revision-Date: 2026-09-02T16:20:40.240Z\n" +"POT-Creation-Date: 2026-09-02T16:31:51.892Z\n" +"PO-Revision-Date: 2026-09-02T16:31:51.892Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx index a20add4340..8f902a6dcd 100644 --- a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx +++ b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.component.tsx @@ -22,9 +22,9 @@ const getStyles = () => ({ type Props = PlainProps & WithStyles; -const StageEventHeaderPlain = ({ icon, title, events, classes }: Props) => { - const eventLabel = useTermLabel('event'); - const eventsLabel = useTermLabel('event', { plural: true }); +const StageEventHeaderPlain = ({ icon, title, events, programId, stageId, classes }: Props) => { + const eventLabel = useTermLabel('event', { programId, stageId }); + const eventsLabel = useTermLabel('event', { programId, stageId, plural: true }); return (
{ diff --git a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.types.ts b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.types.ts index 270dfb455a..be2b845336 100644 --- a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.types.ts +++ b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventHeader/StageEventHeader.types.ts @@ -5,4 +5,6 @@ export type PlainProps = { title?: string; events: Array; icon?: Icon; + programId: string; + stageId?: string; }; diff --git a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventList.component.tsx b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventList.component.tsx index aaec14c6df..a918f70c77 100644 --- a/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventList.component.tsx +++ b/src/core_modules/capture-core/components/Pages/StageEvent/StageEventList/StageEventList.component.tsx @@ -25,6 +25,8 @@ const StageEventListPlain = ({ stage, programId, ...passOnProps }: PlainProps) = title={stage?.name} icon={stage?.icon} events={[]} + programId={programId} + stageId={stage?.id} />} > {programType === programTypes.EVENT_PROGRAM && { if (!orgUnitLoading && (data as any)?.length === 1) { diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx index 73cece0548..7761b8465a 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx @@ -25,7 +25,10 @@ const styles: Readonly = { }; const WidgetTwoEventWorkspacePlain = ({ linkedEvent, dataValues, formFoundation, classes }: Props) => { - const orgUnitLabel = capitalizeFirstLetter(useTermLabel('orgUnit')); + const orgUnitLabel = capitalizeFirstLetter(useTermLabel('orgUnit', { + programId: linkedEvent?.program, + stageId: linkedEvent?.programStage, + })); const dataEntryValues = useMemo(() => getDataEntryDetails( linkedEvent, formFoundation, diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx index 92acf83b7b..902de0500f 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx @@ -6,12 +6,13 @@ import { tCustomTerm } from '../../../../utils/tCustomTerm'; type Props = { eventId: string; + programId: string; onClose: () => void; onConfirmDelete: (eventId: string) => void; }; -export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) => { - const eventLabel = useTermLabel('event'); +export const DeleteEventModal = ({ eventId, programId, onClose, onConfirmDelete }: Props) => { + const eventLabel = useTermLabel('event', { programId }); const handleConfirm = () => { onConfirmDelete(eventId); onClose(); diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx index c6eb8aeab7..7dfdef15aa 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx @@ -69,6 +69,7 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas {deleteModalOpen && eventIdToDelete && ( diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts index 2cb62dca58..f0bf862f35 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/useDefaultColumnConfiguration/useDefaultColumnConfig.ts @@ -68,7 +68,7 @@ const getMetaDataConfig = (stage: ProgramStage): Array => })) as Array; export const useDefaultColumnConfig = (stage: ProgramStage): EventWorkingListsColumnConfigs => { - const orgUnitLabel = useTermLabel('orgUnit'); + const orgUnitLabel = useTermLabel('orgUnit', { stageId: stage.id }); return useMemo(() => [ ...getDefaultMainConfig(stage, orgUnitLabel), ...getMetaDataConfig(stage), diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts index 3b3dcfba83..a2c3843ccc 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts @@ -143,7 +143,7 @@ export const useDefaultColumnConfig = ( orgUnitId: string | null | undefined, programStageId: string | null | undefined, ): TrackerWorkingListsColumnConfigs => { - const orgUnitLabel = useTermLabel('orgUnit'); + const orgUnitLabel = useTermLabel('orgUnit', { programId: program.id }); return useMemo(() => { const { attributes, stages } = program; const searchFilterMetaById = buildSearchFilterMetaById(program); diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts index b1aacf00a6..1cab61637e 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useFiltersOnly.ts @@ -7,11 +7,11 @@ import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import { MAIN_FILTERS } from '../../constants'; export const useFiltersOnly = ( - { enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, + { id: programId, enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, programStageId?: string, ) => { - const enrollmentLabel = useTermLabel('enrollment'); - const followUpLabel = useTermLabel('followUp'); + const enrollmentLabel = useTermLabel('enrollment', { programId }); + const followUpLabel = useTermLabel('followUp', { programId }); return useMemo(() => { const enableUserAssignment = !programStageId && Array.from(stages.values()).find((stage: any) => stage.enableUserAssignment); From 0e6a407a12f6848f9e120923aa65c87bb6df2565 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:12:20 +0000 Subject: [PATCH 063/118] fix: enhance withCustomLabels to include programId and stageId in label retrieval --- i18n/en.pot | 4 ++-- src/core_modules/capture-core/HOC/withCustomLabels.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 69bf7d9e86..7b2a45a89e 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T08:08:57.448Z\n" -"PO-Revision-Date: 2026-09-02T08:08:57.448Z\n" +"POT-Creation-Date: 2026-09-02T17:12:22.225Z\n" +"PO-Revision-Date: 2026-09-02T17:12:22.225Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/HOC/withCustomLabels.tsx b/src/core_modules/capture-core/HOC/withCustomLabels.tsx index 659b0957ad..d7ebf4d698 100644 --- a/src/core_modules/capture-core/HOC/withCustomLabels.tsx +++ b/src/core_modules/capture-core/HOC/withCustomLabels.tsx @@ -15,11 +15,12 @@ type InjectedLabels = { [K in keyof S]: string }; export const withCustomLabels = (specs: S) =>

>(WrappedComponent: React.ComponentType

>) => - (props: P) => { + (props: P & { programId?: string; stageId?: string }) => { + const { programId, stageId } = props; const labels = Object.fromEntries( Object.entries(specs).map(([propName, { key, plural }]) => [ propName, - capitalizeFirstLetter(useTermLabel(key, { plural })), + capitalizeFirstLetter(useTermLabel(key, { programId, stageId, plural })), ]), ) as InjectedLabels; return React.createElement(WrappedComponent, { ...props, ...labels }); From 203c99a0a9c5c9d9ffda9fb113c7872cfffa9331 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:18:31 +0000 Subject: [PATCH 064/118] feat: integrating programId and stageId across components --- i18n/en.pot | 64 +------- .../capture-core/HOC/withCustomLabels.tsx | 5 +- .../DataEntry/DataEntry.component.tsx | 3 +- ...SingleEventRegistrationEntry.component.tsx | 9 +- .../Enrollment/MissingMessage.component.tsx | 3 +- .../NewEventWorkspace.component.tsx | 6 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 9 +- .../ReadOnlyBadge/ReadOnlyBadge.types.ts | 1 + .../OrgUnitFetcher.component.tsx | 2 +- .../EditEventDataEntry.component.tsx | 21 ++- .../EditEventDataEntry.container.ts | 2 + .../WidgetProfile/WidgetProfile.component.tsx | 8 +- .../Stages/Stage/Stage.component.tsx | 1 + .../StageDetail/StageDetail.component.tsx | 4 +- .../Stage/StageDetail/hooks/useEventList.ts | 4 +- .../StageOverview/StageOverview.component.tsx | 7 +- .../StageOverview/stageOverview.types.ts | 1 + .../WidgetStagesAndEvents.component.tsx | 9 +- ...NewTrackedEntityRelationship.component.tsx | 5 +- ...NewTrackedEntityRelationship.container.tsx | 6 +- .../hooks/useAddRelationship.ts | 6 +- ...getTrackedEntityRelationship.component.tsx | 12 +- .../DeleteRelationship/DeleteRelationship.tsx | 19 ++- .../useDeleteRelationship.ts | 6 +- .../LinkedEntityTableBody.component.tsx | 143 ++++++++++-------- ...portedAttributesNotification.component.tsx | 3 +- 26 files changed, 189 insertions(+), 170 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fb99f61965..fbea9d31fc 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T16:31:51.892Z\n" -"PO-Revision-Date: 2026-09-02T16:31:51.892Z\n" +"POT-Creation-Date: 2026-09-02T17:18:33.206Z\n" +"PO-Revision-Date: 2026-09-02T17:18:33.206Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -157,9 +157,6 @@ msgstr "Assigned user" msgid "Search for user" msgstr "Search for user" -msgid "Complete event" -msgstr "Complete event" - msgid "Notes" msgstr "Notes" @@ -230,9 +227,6 @@ msgstr "Yes, discard changes" msgid "No, cancel" msgstr "No, cancel" -msgid "You don't have access to create an event in the current selections" -msgstr "You don't have access to create an event in the current selections" - msgid "Saving a {{trackedEntityName}}" msgstr "Saving a {{trackedEntityName}}" @@ -588,9 +582,6 @@ msgstr "Enroll {{teiDisplayName}} in this program." msgid "Enroll a new {{selectedTetName}} in this program." msgstr "Enroll a new {{selectedTetName}} in this program." -msgid "Create a new event in this program." -msgstr "Create a new event in this program." - msgid "View working list in this program." msgstr "View working list in this program." @@ -1095,15 +1086,6 @@ msgstr "Error" msgid "Warning" msgstr "Warning" -msgid "Go to “Schedule” tab to reschedule this event" -msgstr "Go to “Schedule” tab to reschedule this event" - -msgid "Scheduled date cannot be changed for {{ eventStatus }} events" -msgstr "Scheduled date cannot be changed for {{ eventStatus }} events" - -msgid "You do not have access to uncomplete this event" -msgstr "You do not have access to uncomplete this event" - msgid "Geometry (Area)" msgstr "Geometry (Area)" @@ -1235,12 +1217,6 @@ msgstr "View profile" msgid "Profile widget could not be loaded. Please try again later" msgstr "Profile widget could not be loaded. Please try again later" -msgid "No attributes configured for {{trackedEntityTypeName}}" -msgstr "No attributes configured for {{trackedEntityTypeName}}" - -msgid "No attributes configured" -msgstr "No attributes configured" - msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" @@ -1340,42 +1316,9 @@ msgstr "New {{trackedEntityTypeName}} relationship" msgid "Missing implementation step" msgstr "Missing implementation step" -msgid "Go back without saving relationship" -msgstr "Go back without saving relationship" - -msgid "New Relationship" -msgstr "New Relationship" - msgid "Link to an existing {{tetName}}" msgstr "Link to an existing {{tetName}}" -msgid "An error occurred while adding the relationship" -msgstr "An error occurred while adding the relationship" - -msgid "Something went wrong while loading relationships. Please try again later." -msgstr "Something went wrong while loading relationships. Please try again later." - -msgid "{{trackedEntityTypeName}} relationships" -msgstr "{{trackedEntityTypeName}} relationships" - -msgid "Delete relationship" -msgstr "Delete relationship" - -msgid "Deleting the relationship is permanent and cannot be undone." -msgstr "Deleting the relationship is permanent and cannot be undone." - -msgid "Are you sure you want to delete this relationship?" -msgstr "Are you sure you want to delete this relationship?" - -msgid "Yes, delete relationship" -msgstr "Yes, delete relationship" - -msgid "An error occurred while deleting the relationship." -msgstr "An error occurred while deleting the relationship." - -msgid "To open this relationship, please wait until saving is complete" -msgstr "To open this relationship, please wait until saving is complete" - msgid "Type" msgstr "Type" @@ -1634,9 +1577,6 @@ msgstr "Please enter a valid time" msgid "Please enter a time" msgstr "Please enter a time" -msgid "Some attributes are hidden" -msgstr "Some attributes are hidden" - msgid "Set coordinate" msgstr "Set coordinate" diff --git a/src/core_modules/capture-core/HOC/withCustomLabels.tsx b/src/core_modules/capture-core/HOC/withCustomLabels.tsx index 659b0957ad..d7ebf4d698 100644 --- a/src/core_modules/capture-core/HOC/withCustomLabels.tsx +++ b/src/core_modules/capture-core/HOC/withCustomLabels.tsx @@ -15,11 +15,12 @@ type InjectedLabels = { [K in keyof S]: string }; export const withCustomLabels = (specs: S) =>

>(WrappedComponent: React.ComponentType

>) => - (props: P) => { + (props: P & { programId?: string; stageId?: string }) => { + const { programId, stageId } = props; const labels = Object.fromEntries( Object.entries(specs).map(([propName, { key, plural }]) => [ propName, - capitalizeFirstLetter(useTermLabel(key, { plural })), + capitalizeFirstLetter(useTermLabel(key, { programId, stageId, plural })), ]), ) as InjectedLabels; return React.createElement(WrappedComponent, { ...props, ...labels }); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index a079328b20..a83995e0d3 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -16,6 +16,7 @@ import { getEventDateValidatorContainers, getOrgUnitValidatorContainers } from ' import { type RenderFoundation } from '../../../../../metaData'; import { withMainButton } from './withMainButton'; import { getNoteValidatorContainers } from './fieldValidators/note.validatorContainersGetter'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; import { withSaveHandler, placements, @@ -328,7 +329,7 @@ const buildCompleteFieldSettingsFn = () => { const completeSettings = { getComponent: () => completeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Complete event'), + label: tCustomTerm('Complete {{eventLabel}}', { eventLabel: props.eventLabel }), id: 'complete', }), getPropName: () => 'complete', diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx index 6843b28f2a..8daaea30c6 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx @@ -1,19 +1,24 @@ import React from 'react'; import { useDispatch } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; import { NewEventDataEntryWrapper } from './DataEntryWrapper/NewEventDataEntryWrapper.container'; import { NewRelationshipWrapper } from './NewRelationshipWrapper/NewEventNewRelationshipWrapper.container'; import { cancelNewEventAndReturnToMainPage } from './DataEntryWrapper/DataEntry/actions/dataEntry.actions'; import type { Props } from './SingleEventRegistrationEntry.types'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const SingleEventRegistrationEntryComponent = ({ showAddRelationship, eventAccess }: Props) => { const dispatch = useDispatch(); + const eventLabel = useTermLabel('event'); if (!eventAccess.write) { return ( dispatch(cancelNewEventAndReturnToMainPage())} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index fd35cef593..7c2aab0234 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -188,6 +188,7 @@ const MissingMessagePlain = ({ const enrollmentLabel = useTermLabel('enrollment'); const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); const orgUnitLabel = useTermLabel('orgUnit'); + const eventLabel = useTermLabel('event'); const { trackedEntityName: tetName } = useScopeInfo(tetId); const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId); @@ -315,7 +316,7 @@ const MissingMessagePlain = ({ className={classes.link} onClick={navigateToEventProgramRegistrationPage} > - {i18n.t('Create a new event in this program.')} + {tCustomTerm('Create a new {{eventLabel}} in this program.', { eventLabel })}

diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx index eef671a61a..0c518e6eed 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/NewEventWorkspace/NewEventWorkspace.component.tsx @@ -50,6 +50,7 @@ const NewEventWorkspacePlain = ({ const tempMode = useRef(undefined); const { stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); const programStageLabel = useTermLabel('programStage', { programId, stageId }); + const eventLabel = useTermLabel('event', { programId, stageId }); const onHandleSwitchTab = (newMode: string) => { if (dataEntryHasChanges) { @@ -82,7 +83,10 @@ const NewEventWorkspacePlain = ({ return renderWidget(
, ); diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index d187d60696..5ed7bfa4c9 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -76,13 +76,14 @@ const ReadOnlyBadgePlain = ({ trackedEntityName, trackedEntityInactive = false, inlineLabel = false, + programId, stageId, classes, }: Props & WithStyles) => { - const enrollmentLabel = useTermLabel('enrollment'); - const programStageLabel = useTermLabel('programStage', { stageId }); - const programStagesLabel = useTermLabel('programStage', { plural: true }); - const eventLabel = useTermLabel('event', { stageId }); + const enrollmentLabel = useTermLabel('enrollment', { programId }); + const programStageLabel = useTermLabel('programStage', { programId, stageId }); + const programStagesLabel = useTermLabel('programStage', { programId, plural: true }); + const eventLabel = useTermLabel('event', { programId, stageId }); const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts index e56bebaf20..dc83d82818 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts @@ -9,6 +9,7 @@ export type Props = { trackedEntityName?: string; trackedEntityInactive?: boolean; inlineLabel?: boolean; + programId?: string; stageId?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx index d136bdac1d..fdff10051c 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx @@ -10,7 +10,7 @@ export const OrgUnitFetcher = ({ ...passOnProps }: OrgUnitFetcherProps) => { const { error, orgUnit } = useCoreOrgUnit(orgUnitId); - const orgUnitLabel = useTermLabel('orgUnit'); + const orgUnitLabel = useTermLabel('orgUnit', { programId: passOnProps.program?.id }); if (error) { return ( diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx index e4d69d78c0..aba0358c4a 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx @@ -37,6 +37,7 @@ import { } from '../../FormFields/New'; import { statusTypes, translatedStatusTypes } from '../../../events/statusTypes'; import { eventStatuses } from '../constants/status.const'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import labelTypeClasses from '../DataEntry/dataEntryFieldLabels.module.css'; import { withDeleteButton } from '../DataEntry/withDeleteButton'; import { withAskToCreateNew } from '../../DataEntry/withAskToCreateNew'; @@ -166,10 +167,17 @@ const buildScheduleDateSettingsFn = () => { const isScheduleableStatus = [statusTypes.SCHEDULE, statusTypes.OVERDUE].includes(innerProps.eventStatus); + const eventLabel = innerProps.eventLabel; + const eventsLabel = innerProps.eventsLabel; return isScheduleableStatus ? - i18n.t('Go to “Schedule” tab to reschedule this event') : - i18n.t('Scheduled date cannot be changed for {{ eventStatus }} events', - { eventStatus: translatedStatusTypes()[innerProps.eventStatus] }); + tCustomTerm( + 'Go to “Schedule” tab to reschedule this {{eventLabel}}', + { eventLabel }, + ) : + tCustomTerm( + 'Scheduled date cannot be changed for {{ eventStatus }} {{eventsLabel}}', + { eventStatus: translatedStatusTypes()[innerProps.eventStatus], eventsLabel }, + ); }, })( withDisplayMessages()( @@ -328,7 +336,10 @@ const buildCompleteFieldSettingsFn = () => { const canUncompleteEvent = props.canUncompleteEvent; const shouldDisable = isEventCompleted && !canUncompleteEvent; return shouldDisable - ? i18n.t('You do not have access to uncomplete this event') + ? tCustomTerm( + 'You do not have access to uncomplete this {{eventLabel}}', + { eventLabel: props.eventLabel }, + ) : undefined; })(TrueOnlyField), ), @@ -346,7 +357,7 @@ const buildCompleteFieldSettingsFn = () => { const shouldDisable = isEventCompleted && !canUncompleteEvent; return createComponentProps(props, { - label: i18n.t('Complete event'), + label: tCustomTerm('Complete {{eventLabel}}', { eventLabel: props.eventLabel }), id: 'complete', disabled: shouldDisable, eventStatus: props.eventStatus, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts index d1a20bd242..76a23bbeda 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts @@ -28,6 +28,8 @@ import { getLocationQuery } from '../../../utils/routing/getLocationQuery'; const customLabels = { orgUnitLabel: { key: 'orgUnit' }, + eventLabel: { key: 'event' }, + eventsLabel: { key: 'event', plural: true }, } as const; const mapStateToProps = (state: any, props: any) => { diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index fbfcd015a9..44d7fff5a0 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -11,6 +11,8 @@ import { Widget } from '../Widget'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { NoticeBox } from '../NoticeBox'; import type { Props } from './widgetProfile.types'; +import { useTermLabel } from '../../metaData'; +import { tCustomTerm } from '../../utils/tCustomTerm'; import { useProgram, useTrackedEntityInstances, @@ -80,6 +82,7 @@ const WidgetProfilePlain = ({ const queryClient = useQueryClient(); const [open, setOpenStatus] = useState(true); const [modalState, setTeiModalState] = useState(TEI_MODAL_STATE.CLOSE); + const attributeLabel = useTermLabel('attribute', { programId }); const { loading: programsLoading, program, error: programsError } = useProgram(programId); const { storedAttributeValues, storedGeometry, hasError } = useSelector(({ trackedEntityInstance }: any) => ({ storedAttributeValues: trackedEntityInstance?.attributeValues, @@ -176,11 +179,12 @@ const WidgetProfilePlain = ({

{trackedEntityTypeName - ? i18n.t('No attributes configured for {{trackedEntityTypeName}}', { + ? tCustomTerm('No {{attributeLabel}} configured for {{trackedEntityTypeName}}', { + attributeLabel, trackedEntityTypeName, interpolation: { escapeValue: false }, }) - : i18n.t('No attributes configured')} + : tCustomTerm('No {{attributeLabel}} configured', { attributeLabel })}

); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index b77458a9f2..722576efe4 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -53,6 +53,7 @@ export const StagePlain = ({ description={description} events={events} stageWriteAccess={effectiveStageWriteAccess} + programId={passOnProps.programId} stageId={id} />} onOpen={handleOpen} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index bfcc3dc663..f85911b814 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx @@ -113,7 +113,9 @@ const StageDetailPlain = (props: Props & WithStyles) => { const eventsLabel = useTermLabel('event', { programId, stageId, plural: true }); const { stageWriteAccessById } = useEnrollmentAccessContext(); const stageWriteAccess = stageWriteAccessById[stageId] ?? stage?.access?.data?.write; - const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); + const headerColumns = useComputeHeaderColumn( + dataElements, hideDueDate, enableUserAssignment, stage?.stageForm, programId, stageId, + ); const dataElementsClient = useClientDataElements(dataElements); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElementsClient, events); diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts index e2feddcbc2..413a3b110c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts @@ -118,8 +118,10 @@ const useComputeHeaderColumn = ( hideDueDate: boolean, enableUserAssignment: boolean, formFoundation?: { getLabel: (key: string) => string }, + programId?: string, + stageId?: string, ) => { - const orgUnitLabel = capitalizeFirstLetter(useTermLabel('orgUnit')); + const orgUnitLabel = capitalizeFirstLetter(useTermLabel('orgUnit', { programId, stageId })); const headerColumns = useMemo(() => { const dataElementHeaders = dataElements.reduce((acc, currDataElement) => { const { id, name, formName, type, optionSet } = currDataElement; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index 1038841a9d..f78908681e 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx @@ -94,7 +94,7 @@ const getLastUpdatedAt = (events: Array, fromServerDate: (da }; export const StageOverviewPlain = ({ - title, icon, description, events, stageWriteAccess = true, stageId, classes, + title, icon, description, events, stageWriteAccess = true, programId, stageId, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); const { anyStageWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); @@ -102,8 +102,8 @@ export const StageOverviewPlain = ({ const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; const scheduledEvents = events.filter(event => event.status === statusTypes.SCHEDULE).length; - const eventLabel = useTermLabel('event', { stageId }); - const eventsLabel = useTermLabel('event', { stageId, plural: true }); + const eventLabel = useTermLabel('event', { programId, stageId }); + const eventsLabel = useTermLabel('event', { programId, stageId, plural: true }); return (
@@ -168,6 +168,7 @@ export const StageOverviewPlain = ({ {showStageBadge && ( )} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts index 80eb891438..6bca6f9d61 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts @@ -7,5 +7,6 @@ export type Props = { icon?: Icon; description?: string | null; stageWriteAccess?: boolean; + programId?: string; stageId: string; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index db9f69b6c1..d226987ece 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -31,6 +31,7 @@ const WidgetStagesAndEventsPlain = ({ }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const programStagesLabel = useTermLabel('programStage', { programId, plural: true }); + const eventsLabel = useTermLabel('event', { programId, plural: true }); const { anyStageWriteAccess, anyStageReadAccess, @@ -46,12 +47,18 @@ const WidgetStagesAndEventsPlain = ({ - {tCustomTerm('{{programStagesLabel}} and events', { programStagesLabel })} + + {tCustomTerm( + '{{programStagesLabel}} and {{eventsLabel}}', + { programStagesLabel, eventsLabel }, + )} + {showWidgetBadge && (
)} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.tsx index 08a3152316..8a6095c441 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.tsx @@ -3,6 +3,8 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../../../Widget'; import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; import { Breadcrumbs } from './Breadcrumbs'; import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS, type WizardStep } from './wizardSteps.const'; import { @@ -50,6 +52,7 @@ const NewTrackedEntityRelationshipPlain = ({ onSelectFindMode, classes, }: ComponentProps & WithStyles) => { + const relationshipLabel = useTermLabel('relationship', { programId }); const [currentStep, setCurrentStep] = useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); const [selectedLinkedEntityMetadata, setSelectedLinkedEntityMetadata] = @@ -275,7 +278,7 @@ const NewTrackedEntityRelationshipPlain = ({
- {i18n.t('Go back without saving relationship')} + {tCustomTerm('Go back without saving {{relationshipLabel}}', { relationshipLabel })}
) => { const [addWizardVisible, setAddWizardVisible] = useState(false); + const relationshipLabel = useTermLabel('relationship', { programId }); const closeAddWizard = useCallback(() => { setAddWizardVisible(false); @@ -48,7 +50,7 @@ const NewTrackedEntityRelationshipPlain = ({ small secondary > - {i18n.t('New Relationship')} + {tCustomTerm('New {{relationshipLabel}}', { relationshipLabel })} )} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.ts index bdf38e55de..71d64afbd6 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.ts @@ -1,8 +1,9 @@ -import i18n from '@dhis2/d2-i18n'; import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { handleAPIResponse, REQUESTED_ENTITIES } from 'capture-core/utils/api'; import type { Mutation } from 'capture-core-utils/types/app-runtime'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; type Props = { teiId: string; @@ -21,8 +22,9 @@ const addRelationshipMutation: Mutation = { export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { const queryClient = useQueryClient(); const dataEngine = useDataEngine(); + const relationshipLabel = useTermLabel('relationship'); const { show: showAlert } = useAlert( - i18n.t('An error occurred while adding the relationship'), + tCustomTerm('An error occurred while adding the {{relationshipLabel}}', { relationshipLabel }), { critical: true }, ); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 34b9048387..b336d23d62 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -1,11 +1,12 @@ import React, { useMemo } from 'react'; -import i18n from '@dhis2/d2-i18n'; import type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; import { RelationshipsWidget } from '../common/RelationshipsWidget'; import { RelationshipSearchEntities, useRelationships } from '../common/useRelationships'; import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship'; import { useTrackedEntityTypeName } from './hooks/useTrackedEntityTypeName'; import { useRelationshipTypes } from '../common/RelationshipsWidget/useRelationshipTypes'; +import { useTermLabel } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; export const WidgetTrackedEntityRelationship = ({ relationshipTypes: cachedRelationshipTypes, @@ -27,6 +28,7 @@ export const WidgetTrackedEntityRelationship = ({ }: WidgetTrackedEntityRelationshipProps) => { const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: trackedEntityTypeName, isLoading: isLoadingTEType } = useTrackedEntityTypeName(trackedEntityTypeId); + const relationshipLabel = useTermLabel('relationship', { programId }); const { data: relationships, isError, @@ -44,7 +46,10 @@ export const WidgetTrackedEntityRelationship = ({ if (isError) { return (
- {i18n.t('Something went wrong while loading relationships. Please try again later.')} + {tCustomTerm( + 'Something went wrong while loading {{relationshipLabel}}. Please try again later.', + { relationshipLabel }, + )}
); } @@ -55,8 +60,9 @@ export const WidgetTrackedEntityRelationship = ({ return ( = { tableCell: { @@ -28,6 +30,7 @@ export const DeleteRelationshipPlain = ({ classes, }: Props & WithStyles) => { const [isModalOpen, setIsModalOpen] = useState(false); + const relationshipLabel = useTermLabel('relationship'); return ( <> @@ -48,11 +51,19 @@ export const DeleteRelationshipPlain = ({ onClose={() => setIsModalOpen(false)} dataTest={'delete-relationship-modal'} > - {i18n.t('Delete relationship')} + + {tCustomTerm('Delete {{relationshipLabel}}', { relationshipLabel })} + - {i18n.t('Deleting the relationship is permanent and cannot be undone.')} + {tCustomTerm( + 'Deleting the {{relationshipLabel}} is permanent and cannot be undone.', + { relationshipLabel }, + )} {' '} - {i18n.t('Are you sure you want to delete this relationship?')} + {tCustomTerm( + 'Are you sure you want to delete this {{relationshipLabel}}?', + { relationshipLabel }, + )} @@ -69,7 +80,7 @@ export const DeleteRelationshipPlain = ({ setIsModalOpen(false); }} > - {i18n.t('Yes, delete relationship')} + {tCustomTerm('Yes, delete {{relationshipLabel}}', { relationshipLabel })} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/useDeleteRelationship.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/useDeleteRelationship.ts index 52d4e73289..cf9f73a05a 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/useDeleteRelationship.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/useDeleteRelationship.ts @@ -1,10 +1,11 @@ -import i18n from '@dhis2/d2-i18n'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import { handleAPIResponse, REQUESTED_ENTITIES } from 'capture-core/utils/api'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { ReactQueryAppNamespace } from '../../../../../utils/reactQueryHelpers'; +import { useTermLabel } from '../../../../../metaData'; +import { tCustomTerm } from '../../../../../utils/tCustomTerm'; type Props = { sourceId: string; @@ -27,8 +28,9 @@ const deleteRelationshipMutation = { export const useDeleteRelationship = ({ sourceId }: Props): { onDeleteRelationship: OnDeleteRelationship } => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const relationshipLabel = useTermLabel('relationship'); const { show: showError } = useAlert( - i18n.t('An error occurred while deleting the relationship.'), + tCustomTerm('An error occurred while deleting the {{relationshipLabel}}.', { relationshipLabel }), { critical: true, }, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx index 8e1d74bbe3..aede275aa6 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx @@ -7,11 +7,12 @@ import { DataTableCell, Tooltip, } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { convertServerToClient } from '../../../../converters'; import { convert as convertClientToList } from '../../../../converters/clientToList'; import type { Props } from './linkedEntityTableBody.types'; import { DeleteRelationship } from './DeleteRelationship'; +import { useTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const styles: Readonly = { row: { @@ -30,73 +31,81 @@ const LinkedEntityTableBodyPlain = ({ context, onDeleteRelationship, classes, -}: Props & WithStyles) => ( - - { - linkedEntities - .map(({ id: entityId, values, baseValues, navigation }) => { - const { pendingApiResponse, relationshipId } = baseValues || {}; - return ( - - { - columns.map(({ id, type, options, convertValue }: any) => { - const value = type ? - convertClientToList(convertServerToClient(values[id], type), type, options) : - convertValue(baseValues?.[id] ?? context.display[id]); +}: Props & WithStyles) => { + const relationshipLabel = useTermLabel('relationship'); + return ( + + { + linkedEntities + .map(({ id: entityId, values, baseValues, navigation }) => { + const { pendingApiResponse, relationshipId } = baseValues || {}; + return ( + + { + columns.map(({ id, type, options, convertValue }: any) => { + const value = type ? + convertClientToList(convertServerToClient(values[id], type), type, options) : + convertValue(baseValues?.[id] ?? context.display[id]); - return ( - - {({ onMouseOver, onMouseOut, ref }) => ( - !pendingApiResponse && - onLinkedRecordClick({ ...context.navigation, ...navigation } as any) - } - // @ts-expect-error - UI library expects a ref prop, - // but it is not defined in the types - ref={(tableCell) => { - if (tableCell) { - if (pendingApiResponse) { - tableCell.onmouseover = onMouseOver; - tableCell.onmouseout = onMouseOut; - ref.current = tableCell; - } else { - tableCell.onmouseover = null; - tableCell.onmouseout = null; - } + return ( + + {({ onMouseOver, onMouseOut, ref }) => ( + !pendingApiResponse && + onLinkedRecordClick({ + ...context.navigation, + ...navigation, + } as Parameters[0]) } - }} - > - {value} - - )} - - ); - })} - {context.display.showDeleteButton ? ( - - onDeleteRelationship({ relationshipId: relationshipId! }) - } - disabled={pendingApiResponse} - /> - ) : null} - - ); - }) - } - -); + // @ts-expect-error - UI library expects a ref prop, + // but it is not defined in the types + ref={(tableCell) => { + if (tableCell) { + if (pendingApiResponse) { + tableCell.onmouseover = onMouseOver; + tableCell.onmouseout = onMouseOut; + ref.current = tableCell; + } else { + tableCell.onmouseover = null; + tableCell.onmouseout = null; + } + } + }} + > + {value} + + )} + + ); + })} + {context.display.showDeleteButton ? ( + + onDeleteRelationship({ relationshipId: relationshipId! }) + } + disabled={pendingApiResponse} + /> + ) : null} + + ); + }) + } + + ); +}; export const LinkedEntityTableBody = withStyles(styles)(LinkedEntityTableBodyPlain) as ComponentType; diff --git a/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx b/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx index 841a13fc4d..f03c63e216 100644 --- a/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx +++ b/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import i18n from '@dhis2/d2-i18n'; import { NoticeBox, spacers } from '@dhis2/ui'; import { useTermLabel } from '../../../metaData'; import { tCustomTerm } from '../../tCustomTerm'; @@ -37,7 +36,7 @@ const UnsupportedAttributesNotificationPlain = ({ return (
- + {message}{': '} {unsupportedAttributes.map((attr, index) => ( From 2827c8feb0af430f9103b1ecc376bf8b0608fe04 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:47:59 +0000 Subject: [PATCH 065/118] feat: update terminology usage to include programId for event and enrollment labels --- i18n/en.pot | 29 ++--------------- .../Enrollment/MissingMessage.component.tsx | 8 ++--- .../ProgramStageSelector.component.tsx | 8 ++++- .../ProgramStageSelector.container.tsx | 1 + .../EnrollmentEditEvent/TopBar.container.tsx | 4 +-- .../WithoutOrgUnitSelectedMessage.tsx | 2 +- .../TopBarActions/TopBarActions.component.tsx | 2 +- .../ViewEventDataEntry.component.tsx | 3 +- .../ViewEventDataEntry.container.ts | 1 + .../DeleteModal/DeleteModal.component.tsx | 4 ++- .../RelatedStagesActions.container.tsx | 4 ++- .../ValidationFunctions.ts | 5 +-- .../relatedStageEventIsValid.ts | 2 ++ .../relatedStageEventIsValid.types.ts | 1 + .../Setup/hooks/useDefaultColumnConfig.ts | 9 +++--- .../Actions/CompleteAction/CompleteAction.tsx | 6 +++- .../EnrollmentDeleteModal.tsx | 3 +- .../DeleteTeiAction/DeleteTeiAction.tsx | 7 ++-- .../feedback.reducerDescriptionGetter.ts | 32 +++++++++++++------ 19 files changed, 71 insertions(+), 60 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fbea9d31fc..f92654d76f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T17:18:33.206Z\n" -"PO-Revision-Date: 2026-09-02T17:18:33.206Z\n" +"POT-Creation-Date: 2026-09-02T18:48:00.762Z\n" +"PO-Revision-Date: 2026-09-02T18:48:00.762Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -612,9 +612,6 @@ msgstr "Schedule" msgid "Refer" msgstr "Refer" -msgid "You can't add any more {{ programStageName }} events" -msgstr "You can't add any more {{ programStageName }} events" - msgid "Cancel without saving" msgstr "Cancel without saving" @@ -1098,9 +1095,6 @@ msgstr "Polygon captured" msgid "No polygon captured" msgstr "No polygon captured" -msgid "Event completed" -msgstr "Event completed" - msgid "after" msgstr "after" @@ -1241,9 +1235,6 @@ msgstr "Report date" msgid "Please enter a date" msgstr "Please enter a date" -msgid "Please select a valid event" -msgstr "Please select a valid event" - msgid "Unskip" msgstr "Unskip" @@ -1354,13 +1345,6 @@ msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" msgstr[1] "Delete {{count}} {{ trackedEntityName }}" -msgid "" -"Deleting records will also delete any associated {{enrollmentsLabel}} and " -"events." -msgstr "" -"Deleting records will also delete any associated {{enrollmentsLabel}} and " -"events." - msgid "Are you sure you want to delete?" msgstr "Are you sure you want to delete?" @@ -1501,12 +1485,6 @@ msgstr "Program not found" msgid "Program is not a tracker program" msgstr "Program is not a tracker program" -msgid "Could not save event" -msgstr "Could not save event" - -msgid "Could not delete event" -msgstr "Could not delete event" - msgid "Could not save working list" msgstr "Could not save working list" @@ -1522,9 +1500,6 @@ msgstr "Organisation unit search failed." msgid "Error saving tracked entity instance" msgstr "Error saving tracked entity instance" -msgid "Error editing the event, the changes made were not saved" -msgstr "Error editing the event, the changes made were not saved" - msgid "Error updating the Assignee" msgstr "Error updating the Assignee" diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index 7c2aab0234..7b3bb98abe 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -185,10 +185,10 @@ const MissingMessagePlain = ({ const { resetTeiId } = useResetTeiId(); const { teiDisplayName, tetId } = useSelector(({ enrollmentPage }: any) => enrollmentPage); const { programId, teiId, enrollmentId } = useLocationQuery(); - const enrollmentLabel = useTermLabel('enrollment'); - const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); - const orgUnitLabel = useTermLabel('orgUnit'); - const eventLabel = useTermLabel('event'); + const enrollmentLabel = useTermLabel('enrollment', { programId }); + const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); + const orgUnitLabel = useTermLabel('orgUnit', { programId }); + const eventLabel = useTermLabel('event', { programId }); const { trackedEntityName: tetName } = useScopeInfo(tetId); const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.component.tsx index 9dc3315345..0b551db659 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/ProgramStageSelector/ProgramStageSelector.component.tsx @@ -4,6 +4,8 @@ import { Button, spacers, spacersNum } from '@dhis2/ui'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { NonBundledDhis2Icon } from '../../../NonBundledDhis2Icon'; +import { getTermLabel } from '../../../../metaData'; +import { tCustomTerm } from '../../../../utils/tCustomTerm'; const styles: Readonly = { container: { @@ -39,6 +41,7 @@ type ProgramStage = { type Props = { programStages: ProgramStage[]; + programId: string; onSelectProgramStage: (stageId: string) => void; onCancel: () => void; }; @@ -47,6 +50,7 @@ type ProgramStageSelectorPlainProps = Props & WithStyles; const ProgramStageSelectorComponentPlain = ({ programStages, + programId, onSelectProgramStage, onCancel, classes, @@ -57,13 +61,15 @@ const ProgramStageSelectorComponentPlain = ({ !programStage.dataAccess.write || (!programStage.repeatable && programStage.eventCount > 0) || programStage.hiddenProgramStage; + const eventsLabel = getTermLabel('event', { programId, stageId: programStage.id, plural: true }); return (
diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx index ba268a65a5..e7e8bd213f 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx @@ -47,9 +47,9 @@ export const TopBar = ({ isUserInteractionInProgress, }: Props) => { const { setOrgUnitId } = useSetOrgUnitId(); - const enrollmentLabel = useTermLabel('enrollment', { programId: programId ?? undefined }); + const enrollmentLabel = useTermLabel('enrollment', { programId }); const programStageLabel = useTermLabel('programStage', { - programId: programId ?? undefined, + programId, stageId: programStage?.id, }); diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx index dc708fdca8..1f67b671d0 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx @@ -53,7 +53,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ }: Props) => { const { program, programType } = useProgramInfo(programId); const isTracker = programType === programTypes.TRACKER_PROGRAM; - const orgUnitLabel = useTermLabel('orgUnit'); + const orgUnitLabel = useTermLabel('orgUnit', { programId }); const trackedEntityName = program instanceof TrackerProgram ? program.trackedEntityType?.name diff --git a/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx b/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx index 8c9c5c272e..a65583be98 100644 --- a/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx +++ b/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx @@ -27,7 +27,7 @@ const ActionButtonsPlain = ({ openConfirmDialog, }: PlainProps & WithStyles) => { const { trackedEntityName, scopeType, programName } = useScopeInfo(selectedProgramId); - const eventLabel = useTermLabel('event', { programId: selectedProgramId ?? undefined }); + const eventLabel = useTermLabel('event', { programId: selectedProgramId }); const [openSearch, setOpenSearch] = useState(false); useEffect(() => { diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx index b058ebe9c4..d05eec44a4 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx @@ -14,6 +14,7 @@ import { } from '../../../components/DataEntry'; import { type RenderFoundation, DataElement, dataElementTypes } from '../../../metaData'; +import { tCustomTerm } from '../../../utils/tCustomTerm'; import { convertFormToClient, convertClientToView } from '../../../converters'; import { @@ -223,7 +224,7 @@ const buildCompleteFieldSettingsFn = () => { const completeSettings = { getComponent: () => viewModeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Event completed'), + label: tCustomTerm('{{eventLabel}} completed', { eventLabel: props.eventLabel }), id: dataElement.id, valueConverter: value => dataElement.convertValue(value, valueConvertFn), }), diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts index 6217b64700..7e0698ca59 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts @@ -5,6 +5,7 @@ import { withCustomLabels } from '../../../HOC/withCustomLabels'; const customLabels = { orgUnitLabel: { key: 'orgUnit' }, + eventLabel: { key: 'event' }, } as const; const mapStateToProps = (state: any, props: any) => { diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx index c7677de14b..168600f719 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx @@ -10,6 +10,7 @@ import type { ErrorReport } from '../../processErrorReports'; export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, setOpenModal, onDeleteSuccess }: Props) => { const [errorReports, setErrorReports] = useState>([]); const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); + const eventsLabel = useTermLabel('event', { plural: true }); const handleErrors = (errors: Array) => { setErrorReports(errors); }; @@ -26,10 +27,11 @@ export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, setOpenModal

{/* eslint-disable-next-line max-len */} - {tCustomTerm('Are you sure you want to delete this {{trackedEntityTypeName}}? This will permanently remove the {{trackedEntityTypeName}} and all its associated {{enrollmentsLabel}} and events in all programs.', + {tCustomTerm('Are you sure you want to delete this {{trackedEntityTypeName}}? This will permanently remove the {{trackedEntityTypeName}} and all its associated {{enrollmentsLabel}} and {{eventsLabel}} in all programs.', { trackedEntityTypeName, enrollmentsLabel, + eventsLabel, }, )}

diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx index d597184866..9e2292818b 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx @@ -52,6 +52,7 @@ const RelatedStagesActionsPlain = ({ const { isLoading: orgUnitLoading, data } = useOrgUnitAutoSelect(); const expiryPeriod = useProgramExpiryForUser(programId); const orgUnitLabel = useTermLabel('orgUnit', { programId }); + const eventLabel = useTermLabel('event', { programId, stageId: constraint?.programStage?.id }); useEffect(() => { if (!orgUnitLoading && (data as any)?.length === 1) { @@ -87,8 +88,9 @@ const RelatedStagesActionsPlain = ({ expiryPeriod, setErrorMessages: addErrorMessage, orgUnitLabel, + eventLabel, }); - }, [relatedStageDataValues, expiryPeriod, orgUnitLabel]); + }, [relatedStageDataValues, expiryPeriod, orgUnitLabel, eventLabel]); const getLinkedStageValues = () => ({ linkMode: relatedStageDataValues.linkMode, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts index 9189873f59..4034ab8361 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts @@ -17,6 +17,7 @@ type Props = { expiryDays?: number; }; orgUnitLabel: string; + eventLabel: string; }; export const isScheduledDateValid = ( @@ -114,12 +115,12 @@ const enterData = (props) => { }; const linkToExistingResponse = (props) => { - const { linkedEventId, setErrorMessages } = props ?? {}; + const { linkedEventId, setErrorMessages, eventLabel } = props ?? {}; const linkedEventIdIsValid = !!linkedEventId; if (!linkedEventIdIsValid) { setErrorMessages({ - linkedEventId: i18n.t('Please select a valid event'), + linkedEventId: tCustomTerm('Please select a valid {{eventLabel}}', { eventLabel }), }); } else { setErrorMessages({ diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts index 4dfb26f784..5ba09fe2f6 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts @@ -13,6 +13,7 @@ export const relatedStageWidgetIsValid = ({ setErrorMessages, expiryPeriod, orgUnitLabel, + eventLabel, }: RelatedStageIsValidProps) => { if (!linkMode) { return true; @@ -33,5 +34,6 @@ export const relatedStageWidgetIsValid = ({ setErrorMessages, expiryPeriod, orgUnitLabel, + eventLabel, }); }; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts index 85ce5d2414..c97368188e 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts @@ -17,4 +17,5 @@ export type RelatedStageIsValidProps = { expiryDays?: number; }; orgUnitLabel: string; + eventLabel: string; }; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts index a2c3843ccc..ad8086eda6 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts @@ -28,7 +28,7 @@ const getMainConfig = (hasDisplayInReportsAttributes: boolean, orgUnitLabel: str isMainProperty: true, })); -const getProgramStageMainConfig = (programStage, orgUnitLabel: string): Array => +const getProgramStageMainConfig = (programStage, orgUnitLabel: string, eventLabel: string): Array => [ { id: ADDITIONAL_FILTERS.status, @@ -58,7 +58,7 @@ const getProgramStageMainConfig = (programStage, orgUnitLabel: string): Array { const orgUnitLabel = useTermLabel('orgUnit', { programId: program.id }); + const eventLabel = useTermLabel('event', { programId: program.id, stageId: programStageId }); return useMemo(() => { const { attributes, stages } = program; const searchFilterMetaById = buildSearchFilterMetaById(program); @@ -157,10 +158,10 @@ export const useDefaultColumnConfig = ( if (programStageId && programStage) { return defaultColumns.concat([ - ...getProgramStageMainConfig(programStage, orgUnitLabel), + ...getProgramStageMainConfig(programStage, orgUnitLabel, eventLabel), ...getEventsMetaDataConfig(programStage), ]); } return defaultColumns; - }, [orgUnitId, program, programStageId, orgUnitLabel]); + }, [orgUnitId, program, programStageId, orgUnitLabel, eventLabel]); }; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx index 11bbd78e4f..7e02c5696e 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx @@ -67,6 +67,7 @@ const CompleteActionPlain = ({ const [openAccordion, setOpenAccordion] = useState(false); const enrollmentLabel = useTermLabel('enrollment', { programId }); const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); + const eventsLabel = useTermLabel('event', { programId, plural: true }); const { completeEnrollments, enrollmentCounts, @@ -190,7 +191,10 @@ const CompleteActionPlain = ({ } setCompleteEvents(prevState => !prevState)} /> diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx index b5850f1bb4..1e5f25e8c1 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx @@ -30,6 +30,7 @@ const EnrollmentDeleteModalPlain = ({ }: PlainProps & WithStyles) => { const enrollmentLabel = useTermLabel('enrollment', { programId }); const enrollmentsLabel = useTermLabel('enrollment', { programId, plural: true }); + const eventsLabel = useTermLabel('event', { programId, plural: true }); const { deleteEnrollments, isDeletingEnrollments, @@ -121,7 +122,7 @@ const EnrollmentDeleteModalPlain = ({
{/* eslint-disable-next-line max-len */} - {tCustomTerm('This action will permanently delete the selected {{enrollmentsLabel}}, including all associated data and events.', { enrollmentsLabel })} + {tCustomTerm('This action will permanently delete the selected {{enrollmentsLabel}}, including all associated data and {{eventsLabel}}.', { enrollmentsLabel, eventsLabel })}
diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx index f77db690d5..0050ab8876 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx @@ -20,6 +20,7 @@ export const DeleteTeiAction = ({ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const { hasAuthority } = useAuthority({ authority: CASCADE_DELETE_TEI_AUTHORITY }); const enrollmentsLabel = useTermLabel('enrollment', { plural: true }); + const eventsLabel = useTermLabel('event', { plural: true }); const { deleteTeis, isLoading } = useCascadeDeleteTei({ selectedRows, setIsDeleteDialogOpen, @@ -57,9 +58,9 @@ export const DeleteTeiAction = ({ - {i18n.t( - 'Deleting records will also delete any associated {{enrollmentsLabel}} and events.', - { enrollmentsLabel }, + {tCustomTerm( + 'Deleting records will also delete any associated {{enrollmentsLabel}} and {{eventsLabel}}.', + { enrollmentsLabel, eventsLabel }, )} {' '} {i18n.t('This cannot be undone.')} diff --git a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts index 0f7d841a9e..1900724e0b 100644 --- a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts +++ b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts @@ -82,12 +82,15 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti const errorMessage = isString(error) ? error : error.message; const errorObject = isObject(error) ? error : null; log.error(errorCreator(errorMessage || 'Error saving event')(errorObject)); - return addErrorFeedback({ message: i18n.t('Could not save event') }); + const eventLabel = getTermLabel('event', { programId: action.meta.programId }); + return addErrorFeedback({ message: tCustomTerm('Could not save {{eventLabel}}', { eventLabel }) }); }, [workingListsCommonActionTypes.LIST_UPDATE_ERROR]: (_state, action) => addErrorFeedback({ message: action.payload.errorMessage }), - [eventWorkingListsActionTypes.EVENT_DELETE_ERROR]: () => - addErrorFeedback({ message: i18n.t('Could not delete event') }), + [eventWorkingListsActionTypes.EVENT_DELETE_ERROR]: (_state, action) => { + const eventLabel = getTermLabel('event', { programId: action.meta.programId }); + return addErrorFeedback({ message: tCustomTerm('Could not delete {{eventLabel}}', { eventLabel }) }); + }, [workingListsCommonActionTypes.TEMPLATE_UPDATE_ERROR]: () => addErrorFeedback({ message: i18n.t('Could not save working list') }), [workingListsCommonActionTypes.TEMPLATE_ADD_ERROR]: () => @@ -101,7 +104,8 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti const errorMessage = isString(error) ? error : error.message; const errorObject = isObject(error) ? error : null; log.error(errorCreator(errorMessage || 'Error saving event')(errorObject)); - return addErrorFeedback({ message: i18n.t('Could not save event') }); + const eventLabel = getTermLabel('event', { programId: action.meta.programId }); + return addErrorFeedback({ message: tCustomTerm('Could not save {{eventLabel}}', { eventLabel }) }); }, [dataEntryActionTypes.DATA_ENTRY_RELATIONSHIP_ALREADY_EXISTS]: (_state, action) => addErrorFeedback({ message: action.payload.message }), @@ -118,19 +122,27 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti }); }, [enrollmentSiteActionTypes.SAVE_FAILED]: (_state, action) => { - const enrollmentLabel = getTermLabel('enrollment', { programId: action.payload.programId }); + const programId = action.payload.programId; + const enrollmentLabel = getTermLabel('enrollment', { programId }); + const eventLabel = getTermLabel('event', { programId }); return addErrorFeedback({ - message: tCustomTerm('Error saving the {{enrollmentLabel}} event', { enrollmentLabel }), + message: tCustomTerm('Error saving the {{enrollmentLabel}} {{eventLabel}}', { enrollmentLabel, eventLabel }), }); }, [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: (_state, action) => { - const enrollmentLabel = getTermLabel('enrollment', { programId: action.meta.programId }); + const programId = action.meta.programId; + const enrollmentLabel = getTermLabel('enrollment', { programId }); + const eventLabel = getTermLabel('event', { programId }); + return addErrorFeedback({ + message: tCustomTerm('Error deleting the {{enrollmentLabel}} {{eventLabel}}', { enrollmentLabel, eventLabel }), + }); + }, + [editEventDataEntryAction.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED]: (_state, action) => { + const eventLabel = getTermLabel('event', { programId: action.meta.programId }); return addErrorFeedback({ - message: tCustomTerm('Error deleting the {{enrollmentLabel}} event', { enrollmentLabel }), + message: tCustomTerm('Error editing the {{eventLabel}}, the changes made were not saved', { eventLabel }), }); }, - [editEventDataEntryAction.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error editing the event, the changes made were not saved') }), [enrollmentSiteActionTypes.ERROR_ENROLLMENT]: (_state, action) => addErrorFeedback({ message: i18n.t(action.payload.message) }), [viewEventActionTypes.ASSIGNEE_SAVE_FAILED]: () => From 226b2dc43de69eecb262ec922d4df1e6d5210bda Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:48:31 +0000 Subject: [PATCH 066/118] fix: update TermLabelOptions to allow null values for programId and stageId --- i18n/en.pot | 4 ++-- .../capture-core/metaData/helpers/customLabels.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7b2a45a89e..4a29719dbc 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T17:12:22.225Z\n" -"PO-Revision-Date: 2026-09-02T17:12:22.225Z\n" +"POT-Creation-Date: 2026-09-02T18:48:32.639Z\n" +"PO-Revision-Date: 2026-09-02T18:48:32.639Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts index 6aabb86c77..6142bf5884 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -82,10 +82,10 @@ export const resolveLabel = ( return list.find(source => source?.[target])?.[target]; }; -type TermLabelOptions = LabelOptions & { stageId?: string; programId?: string }; +type TermLabelOptions = LabelOptions & { stageId?: string | null; programId?: string | null }; const resolveTerm = ( - programId: string | undefined, + programId: string | null | undefined, key: CustomLabelKey, { stageId, plural = false }: TermLabelOptions, ): string => { From 790e301cffc590803ef4eb7554898cd1faa28d2a Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:00:17 +0000 Subject: [PATCH 067/118] feat: implement customTerms for improved i18n handling and remove tCustomTerm --- i18n/en.pot | 7 +++- .../WidgetEnrollment.component.tsx | 4 +- .../capture-core/utils/customTerms.ts | 41 +++++++++++++++++++ .../capture-core/utils/tCustomTerm.ts | 37 ----------------- 4 files changed, 48 insertions(+), 41 deletions(-) create mode 100644 src/core_modules/capture-core/utils/customTerms.ts delete mode 100644 src/core_modules/capture-core/utils/tCustomTerm.ts diff --git a/i18n/en.pot b/i18n/en.pot index 4a29719dbc..0f3c882188 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T18:48:32.639Z\n" -"PO-Revision-Date: 2026-09-02T18:48:32.639Z\n" +"POT-Creation-Date: 2026-09-02T19:00:19.764Z\n" +"PO-Revision-Date: 2026-09-02T19:00:19.764Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1446,6 +1446,9 @@ msgstr "Enrollment date" msgid "Incident date" msgstr "Incident date" +msgid "{{enrollmentLabel}} widget could not be loaded. Please try again later" +msgstr "{{enrollmentLabel}} widget could not be loaded. Please try again later" + msgid "Follow-up" msgstr "Follow-up" diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 43f61a5a59..78830c7d04 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -19,12 +19,12 @@ import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDo import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; import { dataElementTypes, useTermLabel } from '../../metaData'; -import { tCustomTerm } from '../../utils/tCustomTerm'; import { convertValue } from '../../converters/clientToView'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; import { Actions } from './Actions'; import { MiniMap } from './MiniMap'; +import { customTerms } from '../../utils/customTerms'; const styles = { enrollment: { @@ -120,7 +120,7 @@ const WidgetEnrollmentPlain = ({ > {initError && (
- {tCustomTerm( + {customTerms.i18n.t( '{{enrollmentLabel}} widget could not be loaded. Please try again later', { enrollmentLabel }, )} diff --git a/src/core_modules/capture-core/utils/customTerms.ts b/src/core_modules/capture-core/utils/customTerms.ts new file mode 100644 index 0000000000..29600a0028 --- /dev/null +++ b/src/core_modules/capture-core/utils/customTerms.ts @@ -0,0 +1,41 @@ +import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; + +type I18nInternal = { + getResource: (lang: string, ns: string, key: string) => string | undefined; + language: string; +}; +const internal = i18n as unknown as I18nInternal; + +const getTranslatedTemplate = (key: string): string => + internal.getResource(internal.language, 'default', key) + ?? internal.getResource('en', 'default', key) + ?? key; + +type Options = { + interpolation?: Record; + [key: string]: unknown; +}; + +export const customTerms = { + i18n: { + t: (key: string, options: Options = {}): string => { + const { interpolation, ...values } = options; + const template = getTranslatedTemplate(key).trimStart(); + + const casedValues = Object.fromEntries( + Object.entries(values).map(([name, value]) => { + const variableIsAtSentenceStart = template.startsWith(`{{${name}}}`) + || template.startsWith(`{{${name},`); + const shouldCapitalize = typeof value === 'string' && variableIsAtSentenceStart; + return [name, shouldCapitalize ? capitalizeFirstLetter(value) : value]; + }), + ); + + return i18n.t(key, { + ...casedValues, + interpolation: { escapeValue: false, ...interpolation }, + }); + }, + }, +}; diff --git a/src/core_modules/capture-core/utils/tCustomTerm.ts b/src/core_modules/capture-core/utils/tCustomTerm.ts deleted file mode 100644 index a1f46accbf..0000000000 --- a/src/core_modules/capture-core/utils/tCustomTerm.ts +++ /dev/null @@ -1,37 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; -import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; - -type I18nInternal = { - getResource: (lang: string, ns: string, key: string) => string | undefined; - language: string; -}; -const internal = i18n as unknown as I18nInternal; - -const getTranslatedTemplate = (key: string): string => - internal.getResource(internal.language, 'default', key) - ?? internal.getResource('en', 'default', key) - ?? key; - -type Options = { - interpolation?: Record; - [key: string]: unknown; -}; - -export const tCustomTerm = (key: string, options: Options = {}): string => { - const { interpolation, ...values } = options; - const template = getTranslatedTemplate(key).trimStart(); - - const casedValues = Object.fromEntries( - Object.entries(values).map(([name, value]) => { - const variableIsAtSentenceStart = template.startsWith(`{{${name}}}`) - || template.startsWith(`{{${name},`); - const shouldCapitalize = typeof value === 'string' && variableIsAtSentenceStart; - return [name, shouldCapitalize ? capitalizeFirstLetter(value) : value]; - }), - ); - - return i18n.t(key, { - ...casedValues, - interpolation: { escapeValue: false, ...interpolation }, - }); -}; From 6ec8e1b1c6616dc0f35f03774df1c0fff9a7b747 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:15:35 +0000 Subject: [PATCH 068/118] fix: replace tCustomTerm with customTerms --- i18n/en.pot | 837 +++++++++++++++++- .../hooks/useOriginLabel.ts | 8 +- .../EnrollmentBreadcrumb.tsx | 10 +- .../hooks/useWorkingListLabel.ts | 8 +- .../EventBreadcrumb/EventBreadcrumb.tsx | 6 +- .../hooks/useWorkingListLabel.ts | 4 +- .../CardList/CardListButtons.component.tsx | 4 +- .../CardList/CardListItem.component.tsx | 4 +- ...lmentWithFirstStageDataEntry.component.tsx | 4 +- .../EnrollmentRegistrationEntry.component.tsx | 4 +- .../DataEntry/DataEntry.component.tsx | 4 +- .../addRelationshipForNewSingleEvent.epics.ts | 6 +- .../note.validatorContainersGetter.ts | 4 +- .../orgUnit.validatorContainersGetter.ts | 4 +- .../RecentlyAddedEventsList.component.tsx | 8 +- ...wEventNewRelationshipWrapper.component.tsx | 16 +- ...SingleEventRegistrationEntry.component.tsx | 4 +- .../CompleteModal/CompleteModal.component.tsx | 20 +- .../dataEntryOutput/withFeedbackOutput.tsx | 4 +- .../dataEntryOutput/withIndicatorOutput.tsx | 4 +- .../DataEntry/withAskToCreateNew.tsx | 8 +- .../DataEntryWidgetOutput.container.ts | 12 +- .../SingleOrgUnitSelectField.component.tsx | 4 +- .../Filters/FiltersRows.component.tsx | 4 +- .../LockedSelector/LockedSelector.epics.ts | 6 +- .../components/Notes/Notes.component.tsx | 6 +- .../EnrollmentPageDefault.container.tsx | 12 +- .../EnrollmentQuickActions.component.tsx | 6 +- .../QuickActionButton/QuickActionButton.tsx | 4 +- .../Enrollment/MissingMessage.component.tsx | 25 +- .../Enrollment/epics/enrollmentPage.epics.ts | 4 +- .../Enrollment/epics/fetchEnrollment.epics.ts | 6 +- ...nrollmentAddEventPageDefault.component.tsx | 12 +- ...nrollmentAddEventPageDefault.container.tsx | 4 +- .../NewEventWorkspace.component.tsx | 6 +- .../WidgetStageHeader.component.tsx | 4 +- .../ProgramStageSelector.component.tsx | 4 +- .../ProgramStageSelector.container.tsx | 6 +- .../EnrollmentEditEventPage.component.tsx | 6 +- ...idCategoryCombinationForOrgUnitMessage.tsx | 4 +- .../NoSelectionsInfoBox.tsx | 4 +- .../WithoutOrgUnitSelectedMessage.tsx | 6 +- .../Header/EventWorkingListsInitHeader.tsx | 4 +- .../Pages/New/NewPage.component.tsx | 6 +- .../RegistrationDataEntry.component.tsx | 4 +- .../DataEntryEnrollment.component.tsx | 4 +- .../StageEventHeader.component.tsx | 4 +- .../EventDetailsSection.component.tsx | 6 +- ...wEventNewRelationshipWrapper.component.tsx | 16 +- .../ViewEventRelationships.epics.ts | 6 +- .../NotesSection/NotesSection.component.tsx | 4 +- .../RelationshipsSection.component.tsx | 4 +- .../ViewEventComponent/ViewEvent.container.ts | 6 +- .../Pages/ViewEvent/epics/editEvent.epics.ts | 12 +- .../Pages/ViewEvent/epics/viewEvent.epics.ts | 8 +- .../DataEntryEnrollment.component.tsx | 4 +- .../SearchOrgUnitSelector.component.tsx | 6 +- .../TeiSearchForm/TeiSearchForm.component.tsx | 4 +- .../WidgetEventEditWrapper.tsx | 6 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 12 +- .../Relationships/Relationships.component.tsx | 6 +- .../OrgUnitSelector.component.tsx | 6 +- .../QuickSelector/Program/ProgramList.tsx | 4 +- .../SearchForm/SearchForm.component.tsx | 4 +- .../SearchOrgUnitSelector.component.tsx | 6 +- .../TeiSearchForm/TeiSearchForm.component.tsx | 4 +- .../TopBarActions/TopBarActions.component.tsx | 4 +- .../WidgetAssignee/DisplayMode.component.tsx | 4 +- .../WidgetBreakingTheGlass.component.tsx | 12 +- .../Actions/Actions.component.tsx | 4 +- .../Actions/AddNew/AddNew.component.tsx | 4 +- .../CompleteModal/CompleteModal.component.tsx | 18 +- .../Actions/Delete/Delete.component.tsx | 21 +- .../Actions/Followup/Followup.component.tsx | 6 +- .../WidgetEnrollment/Date/Date.component.tsx | 7 +- .../InfoBoxes/InfoBoxes.component.tsx | 6 +- .../TransferModal/TransferModal.component.tsx | 4 +- .../WidgetEnrollment.component.tsx | 6 +- .../DataEntry/epics/dataEntryRules.epics.ts | 4 +- .../note.validatorContainersGetter.ts | 4 +- .../orgUnit.validatorContainersGetter.ts | 4 +- .../OrgUnitFetcher.component.tsx | 4 +- .../WidgetEnrollmentEventNew.container.tsx | 4 +- .../WidgetEnrollmentNote.component.tsx | 8 +- .../DataEntry/editEventDataEntry.actions.ts | 4 +- .../epics/editEventDataEntry.epics.ts | 4 +- .../orgUnit.validatorContainersGetter.ts | 4 +- .../DataEntry/withDeleteButton.tsx | 10 +- .../EditEventDataEntry.component.tsx | 10 +- .../ViewEventDataEntry.component.tsx | 4 +- .../viewEventDataEntry.actions.ts | 4 +- .../WidgetHeader/WidgetHeader.container.tsx | 4 +- .../WidgetEventNote.component.tsx | 11 +- .../InfoBox/InfoBox.component.tsx | 4 +- .../ScheduleOrgUnit.component.tsx | 4 +- .../ScheduleText/ScheduleText.component.tsx | 14 +- .../WidgetEventSchedule.component.tsx | 8 +- .../WidgetEventSchedule.container.tsx | 4 +- .../WidgetNote/NoteSection/NoteSection.tsx | 4 +- .../DeleteModal/DeleteModal.component.tsx | 4 +- .../WidgetProfile/WidgetProfile.component.tsx | 6 +- .../EnterData.component.tsx | 4 +- .../LinkToExisting.component.tsx | 6 +- .../RelatedStagesActions.component.tsx | 10 +- .../WidgetRelatedStages.container.tsx | 4 +- .../hooks/useAddEventWithRelationship.ts | 16 +- .../ValidationFunctions.ts | 8 +- .../StageCreateNewButton.tsx | 8 +- .../DeleteActionButton/DeleteActionButton.tsx | 8 +- .../DeleteActionModal/DeleteActionModal.tsx | 17 +- .../EventRow/SkipAction/SkipAction.tsx | 9 +- .../StageDetail/StageDetail.component.tsx | 6 +- .../StageOverview/StageOverview.component.tsx | 4 +- .../Stages/Stages.component.tsx | 4 +- .../WidgetStagesAndEvents.component.tsx | 4 +- .../Modal/UnlinkAndDeleteModal.tsx | 12 +- .../OverflowMenu/Modal/UnlinkModal.tsx | 15 +- .../OverflowMenu/OverflowMenu.component.tsx | 12 +- .../WidgetWrapper/WidgetWrapper.container.tsx | 6 +- ...NewTrackedEntityRelationship.component.tsx | 4 +- ...NewTrackedEntityRelationship.container.tsx | 4 +- .../hooks/useAddRelationship.ts | 4 +- ...getTrackedEntityRelationship.component.tsx | 6 +- .../DeleteRelationship/DeleteRelationship.tsx | 10 +- .../useDeleteRelationship.ts | 4 +- .../LinkedEntityTableBody.component.tsx | 4 +- .../RelationshipsWidget.component.tsx | 4 +- .../useGroupedLinkedEntities.ts | 4 +- .../RowMenuSetup/DeleteEventModal.tsx | 10 +- ...ventWorkingListsRowMenuSetup.component.tsx | 6 +- .../Actions/CompleteAction/CompleteAction.tsx | 14 +- .../hooks/useBulkCompleteEvents.ts | 9 +- .../Actions/DeleteAction/DeleteAction.tsx | 18 +- .../Setup/hooks/useDefaultColumnConfig.ts | 6 +- .../Setup/hooks/useFiltersOnly.ts | 4 +- .../Setup/hooks/useProgramStageFilters.ts | 8 +- .../Setup/hooks/useStaticTemplates.ts | 8 +- .../Actions/CompleteAction/CompleteAction.tsx | 36 +- .../hooks/useCompleteBulkEnrollments.ts | 8 +- .../DeleteEnrollmentsAction.tsx | 6 +- .../EnrollmentDeleteModal.tsx | 22 +- .../hooks/useDeleteEnrollments.ts | 7 +- .../DeleteTeiAction/DeleteTeiAction.tsx | 6 +- .../coreOrgUnit/useCoreOrgUnit.tsx | 4 +- .../feedback.reducerDescriptionGetter.ts | 28 +- .../capture-core/utils/customTerms.ts | 41 + .../capture-core/utils/tCustomTerm.ts | 37 - ...portedAttributesNotification.component.tsx | 6 +- 148 files changed, 1456 insertions(+), 514 deletions(-) create mode 100644 src/core_modules/capture-core/utils/customTerms.ts delete mode 100644 src/core_modules/capture-core/utils/tCustomTerm.ts diff --git a/i18n/en.pot b/i18n/en.pot index c6f27c5282..24a4631246 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-09-02T19:01:28.270Z\n" -"PO-Revision-Date: 2026-09-02T19:01:28.270Z\n" +"POT-Creation-Date: 2026-09-02T19:15:38.687Z\n" +"PO-Revision-Date: 2026-09-02T19:15:38.687Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -42,12 +42,36 @@ msgstr "Bulk data entry" msgid "Program overview" msgstr "Program overview" +msgid "Active {{enrollmentsLabel}}" +msgstr "Active {{enrollmentsLabel}}" + +msgid "Completed {{enrollmentsLabel}}" +msgstr "Completed {{enrollmentsLabel}}" + +msgid "Cancelled {{enrollmentsLabel}}" +msgstr "Cancelled {{enrollmentsLabel}}" + msgid "Search" msgstr "Search" +msgid "{{enrollmentLabel}} dashboard" +msgstr "{{enrollmentLabel}} dashboard" + +msgid "View {{eventLabel}}" +msgstr "View {{eventLabel}}" + +msgid "Edit {{eventLabel}}" +msgstr "Edit {{eventLabel}}" + +msgid "New {{eventLabel}}" +msgstr "New {{eventLabel}}" + msgid "Loading..." msgstr "Loading..." +msgid "{{eventLabel}} list" +msgstr "{{eventLabel}} list" + msgid "More" msgstr "More" @@ -57,6 +81,9 @@ msgstr "View {{programName}} dashboard" msgid "View dashboard" msgstr "View dashboard" +msgid "View active {{enrollmentLabel}}" +msgstr "View active {{enrollmentLabel}}" + msgid "Re-enroll in {{programName}}" msgstr "Re-enroll in {{programName}}" @@ -69,6 +96,9 @@ msgstr "Enrolled" msgid "Previously enrolled" msgstr "Previously enrolled" +msgid "Date of {{enrollmentLabel}}" +msgstr "Date of {{enrollmentLabel}}" + msgid "Last updated" msgstr "Last updated" @@ -108,6 +138,9 @@ msgstr "Area" msgid "Coordinate" msgstr "Coordinate" +msgid "Complete {{eventLabel}}" +msgstr "Complete {{eventLabel}}" + msgid "" "The date entered belongs to an expired period. Enter a date after " "{{firstValidDate}}." @@ -136,6 +169,9 @@ msgstr "Please select {{categoryName}}" msgid "A date in the future is not allowed" msgstr "A date in the future is not allowed" +msgid "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." +msgstr "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." + msgid "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." msgstr "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." @@ -185,12 +221,28 @@ msgstr "" "This is not an event program or the metadata is corrupt. See log for " "details." +msgid "This {{eventLabel}}" +msgstr "This {{eventLabel}}" + +msgid "" +"{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} " +"already exists" +msgstr "" +"{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} " +"already exists" + msgid "Active" msgstr "Active" msgid "Completed" msgstr "Completed" +msgid "Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}" +msgstr "Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}" + +msgid "Please provide a valid {{orgUnitLabel}}" +msgstr "Please provide a valid {{orgUnitLabel}}" + msgid "Save and add another" msgstr "Save and add another" @@ -218,15 +270,42 @@ msgstr "Switch to form view" msgid "Switch to row view" msgstr "Switch to row view" +msgid "{{count}} {{eventLabel}} added" +msgid_plural "{{count}} {{eventLabel}} added" +msgstr[0] "{{count}} {{eventLabel}} added" +msgstr[1] "{{count}} {{eventsLabel}} added" + +msgid "No {{eventsLabel}} added" +msgstr "No {{eventsLabel}} added" + +msgid "Adding {{relationshipLabel}} to {{eventLabel}}." +msgstr "Adding {{relationshipLabel}} to {{eventLabel}}." + +msgid "Go back to {{eventLabel}} without saving {{relationshipLabel}}" +msgstr "Go back to {{eventLabel}} without saving {{relationshipLabel}}" + +msgid "New {{eventLabel}} {{relationshipLabel}}" +msgstr "New {{eventLabel}} {{relationshipLabel}}" + msgid "Discard unsaved changes?" msgstr "Discard unsaved changes?" +msgid "" +"Leaving this page will discard the selections you made for a new " +"{{relationshipLabel}}" +msgstr "" +"Leaving this page will discard the selections you made for a new " +"{{relationshipLabel}}" + msgid "Yes, discard changes" msgstr "Yes, discard changes" msgid "No, cancel" msgstr "No, cancel" +msgid "You don't have access to create an {{eventLabel}} in the current selections" +msgstr "You don't have access to create an {{eventLabel}} in the current selections" + msgid "Saving a {{trackedEntityName}}" msgstr "Saving a {{trackedEntityName}}" @@ -245,6 +324,30 @@ msgstr "An error has occurred. See log for details" msgid "{{programStageName}} completed" msgstr "{{programStageName}} completed" +msgid "" +"Would you like to complete the {{enrollmentLabel}} and all active " +"{{eventsLabel}} as well?" +msgstr "" +"Would you like to complete the {{enrollmentLabel}} and all active " +"{{eventsLabel}} as well?" + +msgid "{{count}} {{eventLabel}} in {{programStageName}}" +msgid_plural "{{count}} {{eventLabel}} in {{programStageName}}" +msgstr[0] "{{count}} {{eventLabel}} in {{programStageName}}" +msgstr[1] "{{count}} {{eventsLabel}} in {{programStageName}}" + +msgid "Yes, complete {{enrollmentLabel}} and {{eventsLabel}}" +msgstr "Yes, complete {{enrollmentLabel}} and {{eventsLabel}}" + +msgid "Complete {{enrollmentLabel}} only" +msgstr "Complete {{enrollmentLabel}} only" + +msgid "Would you like to complete the {{enrollmentLabel}}?" +msgstr "Would you like to complete the {{enrollmentLabel}}?" + +msgid "Complete {{enrollmentLabel}}" +msgstr "Complete {{enrollmentLabel}}" + msgid "A duplicate exists (but there were some errors, see log for details" msgstr "A duplicate exists (but there were some errors, see log for details" @@ -280,6 +383,21 @@ msgstr "Form foundation missing. See log for details" msgid "validation failed" msgstr "validation failed" +msgid "No feedback for this {{eventLabel}} yet" +msgstr "No feedback for this {{eventLabel}} yet" + +msgid "No indicator output for this {{eventLabel}} yet" +msgstr "No indicator output for this {{eventLabel}} yet" + +msgid "Generate new {{eventLabel}}" +msgstr "Generate new {{eventLabel}}" + +msgid "Do you want to create another {{eventLabel}}?" +msgstr "Do you want to create another {{eventLabel}}?" + +msgid "Yes, create new {{eventLabel}}" +msgstr "Yes, create new {{eventLabel}}" + msgid "Back to form" msgstr "Back to form" @@ -301,6 +419,12 @@ msgstr "Some operations are still running. Please wait." msgid "Operations running" msgstr "Operations running" +msgid "No feedback for this {{enrollmentLabel}} yet" +msgstr "No feedback for this {{enrollmentLabel}} yet" + +msgid "No indicator output for this {{enrollmentLabel}} yet" +msgstr "No indicator output for this {{enrollmentLabel}} yet" + msgid "No events to display" msgstr "No events to display" @@ -457,6 +581,9 @@ msgstr "Type to filter options" msgid "No match found" msgstr "No match found" +msgid "Search for an {{orgUnitLabel}}" +msgstr "Search for an {{orgUnitLabel}}" + msgid "Clear" msgstr "Clear" @@ -549,12 +676,27 @@ msgstr "before or equal to" msgid "More filters" msgstr "More filters" +msgid "{{programStageLabel}} filters" +msgstr "{{programStageLabel}} filters" + msgid "Rows per page" msgstr "Rows per page" +msgid "Could not get {{orgUnitLabel}}" +msgstr "Could not get {{orgUnitLabel}}" + msgid "Program doesn't exist" msgstr "Program doesn't exist" +msgid "Selected program is invalid for selected {{orgUnitLabel}}" +msgstr "Selected program is invalid for selected {{orgUnitLabel}}" + +msgid "Add {{noteLabel}}" +msgstr "Add {{noteLabel}}" + +msgid "Write {{noteLabel}}" +msgstr "Write {{noteLabel}}" + msgid "{{fieldName}} was blanked out and hidden by your last action" msgstr "{{fieldName}} was blanked out and hidden by your last action" @@ -567,30 +709,80 @@ msgstr "Close the notice" msgid "Quick actions" msgstr "Quick actions" +msgid "Schedule an {{eventLabel}}" +msgstr "Schedule an {{eventLabel}}" + msgid "Make referral" msgstr "Make referral" +msgid "No available {{programStagesLabel}}" +msgstr "No available {{programStagesLabel}}" + +msgid "Invalid {{enrollmentLabel}} id {{enrollmentId}}." +msgstr "Invalid {{enrollmentLabel}} id {{enrollmentId}}." + +msgid "Choose an {{enrollmentLabel}} to view the dashboard." +msgstr "Choose an {{enrollmentLabel}} to view the dashboard." + +msgid "" +"Choose a program to add new or see existing {{enrollmentsLabel}} for " +"{{teiDisplayName}}" +msgstr "" +"Choose a program to add new or see existing {{enrollmentsLabel}} for " +"{{teiDisplayName}}" + msgid "{{programName}} has categories. Choose all categories to view dashboard." msgstr "{{programName}} has categories. Choose all categories to view dashboard." +msgid "There are no active {{enrollmentsLabel}}." +msgstr "There are no active {{enrollmentsLabel}}." + +msgid "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." +msgstr "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." + msgid "{{teiDisplayName}} is not enrolled in this program." msgstr "{{teiDisplayName}} is not enrolled in this program." msgid "Enroll {{teiDisplayName}} in this program." msgstr "Enroll {{teiDisplayName}} in this program." +msgid "" +"{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " +"{{programName}}. Choose another program that allows {{tetName}} " +"{{enrollmentLabel}}. " +msgstr "" +"{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " +"{{programName}}. Choose another program that allows {{tetName}} " +"{{enrollmentLabel}}. " + msgid "Enroll a new {{selectedTetName}} in this program." msgstr "Enroll a new {{selectedTetName}} in this program." +msgid "{{programName}} is an event program and does not have {{enrollmentsLabel}}." +msgstr "{{programName}} is an event program and does not have {{enrollmentsLabel}}." + +msgid "Create a new {{eventLabel}} in this program." +msgstr "Create a new {{eventLabel}} in this program." + msgid "View working list in this program." msgstr "View working list in this program." +msgid "{{enrollmentLabel}} with id \"{{enrollmentId}}\" does not exist" +msgstr "{{enrollmentLabel}} with id \"{{enrollmentId}}\" does not exist" + msgid "Tracked entity instance with id \"{{teiId}}\" does not exist" msgstr "Tracked entity instance with id \"{{teiId}}\" does not exist" msgid "Program with id \"{{programId}}\" does not exist" msgstr "Program with id \"{{programId}}\" does not exist" +msgid "" +"An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid " +"url." +msgstr "" +"An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid " +"url." + msgid "An error has occurred" msgstr "An error has occurred" @@ -603,6 +795,12 @@ msgstr "There was an error opening the Page" msgid "There was an error loading the page" msgstr "There was an error loading the page" +msgid "{{programStageLabel}} is invalid" +msgstr "{{programStageLabel}} is invalid" + +msgid "{{programStageLabel}} not found" +msgstr "{{programStageLabel}} not found" + msgid "Report" msgstr "Report" @@ -612,9 +810,21 @@ msgstr "Schedule" msgid "Refer" msgstr "Refer" +msgid "You can't add any more {{ programStageName }} {{eventsLabel}}" +msgstr "You can't add any more {{ programStageName }} {{eventsLabel}}" + msgid "Cancel without saving" msgstr "Cancel without saving" +msgid "Choose a {{programStageLabel}} for a new event" +msgstr "Choose a {{programStageLabel}} for a new event" + +msgid "{{programStagesLabel}} could not be loaded" +msgstr "{{programStagesLabel}} could not be loaded" + +msgid "The category option is not valid for the selected {{orgUnitLabel}}." +msgstr "The category option is not valid for the selected {{orgUnitLabel}}." + msgid "Please select a valid combination." msgstr "Please select a valid combination." @@ -624,6 +834,13 @@ msgstr "Get started with Capture app" msgid "Report data" msgstr "Report data" +msgid "" +"Choose a program and {{orgUnitLabel}} to see existing data and create new " +"records." +msgstr "" +"Choose a program and {{orgUnitLabel}} to see existing data and create new " +"records." + msgid "Click 'Search'. For program-specific results, choose a program first." msgstr "Click 'Search'. For program-specific results, choose a program first." @@ -633,9 +850,18 @@ msgstr "Learn more about Capture app" msgid "Please select {{category}}." msgstr "Please select {{category}}." +msgid "Please select an {{orgUnitLabel}}" +msgstr "Please select an {{orgUnitLabel}}" + +msgid "See working list without {{orgUnitLabel}}" +msgstr "See working list without {{orgUnitLabel}}" + msgid "Search for a {{trackedEntityName}}" msgstr "Search for a {{trackedEntityName}}" +msgid "Registered {{eventsLabel}}" +msgstr "Registered {{eventsLabel}}" + msgid "" "You don't have access to create a {{trackedEntityName}} in the current " "selections" @@ -643,6 +869,9 @@ msgstr "" "You don't have access to create a {{trackedEntityName}} in the current " "selections" +msgid "Choose an {{orgUnitLabel}} to start reporting" +msgstr "Choose an {{orgUnitLabel}} to start reporting" + msgid "Choose the {{missingCategories}} to start reporting" msgstr "Choose the {{missingCategories}} to start reporting" @@ -655,12 +884,18 @@ msgstr "New" msgid "You can also choose a program from the top bar and create in that program" msgstr "You can also choose a program from the top bar and create in that program" +msgid "New {{enrollmentLabel}} in program{{escape}} {{programName}}" +msgstr "New {{enrollmentLabel}} in program{{escape}} {{programName}}" + msgid "Save {{trackedEntityTypeName}}" msgstr "Save {{trackedEntityTypeName}}" msgid "Save {{trackedEntityName}}" msgstr "Save {{trackedEntityName}}" +msgid "Enter details now is not available when creating a {{relationshipLabel}}" +msgstr "Enter details now is not available when creating a {{relationshipLabel}}" + msgid "Save new {{trackedEntityTypeName}} and link" msgstr "Save new {{trackedEntityTypeName}} and link" @@ -706,12 +941,33 @@ msgstr "Register" msgid "Back" msgstr "Back" +msgid "{{count}} {{eventLabel}}" +msgid_plural "{{count}} {{eventLabel}}" +msgstr[0] "{{count}} {{eventLabel}}" +msgstr[1] "{{count}} {{eventsLabel}}" + msgid "View changelog" msgstr "View changelog" +msgid "{{eventLabel}} details" +msgstr "{{eventLabel}} details" + +msgid "" +"Leaving this page will discard any selections you made for a new " +"{{relationshipLabel}}" +msgstr "" +"Leaving this page will discard any selections you made for a new " +"{{relationshipLabel}}" + msgid "Errors" msgstr "Errors" +msgid "This {{eventLabel}} doesn't have any notes" +msgstr "This {{eventLabel}} doesn't have any notes" + +msgid "This {{eventLabel}} doesn't have any relationships" +msgstr "This {{eventLabel}} doesn't have any relationships" + msgid "Warnings" msgstr "Warnings" @@ -721,6 +977,12 @@ msgstr "No feedback yet" msgid "No indicator output yet" msgstr "No indicator output yet" +msgid "{{eventLabel}} could not be loaded. Are you sure it exists?" +msgstr "{{eventLabel}} could not be loaded. Are you sure it exists?" + +msgid "{{eventLabel}} could not be loaded" +msgstr "{{eventLabel}} could not be loaded" + msgid "" "Could not load the requested data. It may not exist or you may not have " "access." @@ -728,6 +990,9 @@ msgstr "" "Could not load the requested data. It may not exist or you may not have " "access." +msgid "{{orgUnitLabel}} could not be loaded" +msgstr "{{orgUnitLabel}} could not be loaded" + msgid "Event could not be loaded" msgstr "Event could not be loaded" @@ -740,6 +1005,12 @@ msgstr "All accessible" msgid "Selected" msgstr "Selected" +msgid "Please select an {{orgUnitLabel}}." +msgstr "Please select an {{orgUnitLabel}}." + +msgid "{{orgUnitLabel}} scope" +msgstr "{{orgUnitLabel}} scope" + msgid "organisation unit" msgstr "organisation unit" @@ -752,6 +1023,11 @@ msgstr "Search {{uniqueAttrName}}" msgid "Search by attributes" msgstr "Search by attributes" +msgid "Fill in at least {{count}} {{attributeLabel}} to search" +msgid_plural "Fill in at least {{count}} {{attributeLabel}} to search" +msgstr[0] "Fill in at least {{count}} {{attributeLabel}} to search" +msgstr[1] "Fill in at least {{count}} attributes to search" + msgid "Search {{attributeName}}" msgstr "Search {{attributeName}}" @@ -770,6 +1046,9 @@ msgstr "Search form is missing. See log for details" msgid "Could not retrieve metadata. Please try again later." msgstr "Could not retrieve metadata. Please try again later." +msgid "The {{enrollmentLabel}} event data could not be found" +msgstr "The {{enrollmentLabel}} event data could not be found" + msgid "Loading" msgstr "Loading" @@ -791,6 +1070,9 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" +msgid "You only have view access to this {{enrollmentLabel}}" +msgstr "You only have view access to this {{enrollmentLabel}}" + msgid "You only have view access to this program" msgstr "You only have view access to this program" @@ -800,6 +1082,18 @@ msgstr "You only have view access to this {{trackedEntityName}}" msgid "You only have view access to this tracked entity type" msgstr "You only have view access to this tracked entity type" +msgid "You only have view access to these {{programStagesLabel}}" +msgstr "You only have view access to these {{programStagesLabel}}" + +msgid "You only have view access to this {{programStageLabel}}" +msgstr "You only have view access to this {{programStageLabel}}" + +msgid "This {{eventLabel}} is outside the editing period" +msgstr "This {{eventLabel}} is outside the editing period" + +msgid "This {{eventLabel}} has been completed" +msgstr "This {{eventLabel}} has been completed" + msgid "This {{trackedEntityName}} is deactivated" msgstr "This {{trackedEntityName}} is deactivated" @@ -812,12 +1106,21 @@ msgstr "View only - {{message}}" msgid "View only" msgstr "View only" +msgid "Add {{relationshipLabel}}" +msgstr "Add {{relationshipLabel}}" + msgid "No results found for " msgstr "No results found for " +msgid "Choose an {{orgUnitLabel}} in the form below" +msgstr "Choose an {{orgUnitLabel}} in the form below" + msgid "None selected" msgstr "None selected" +msgid "Choose an {{orgUnitLabel}}" +msgstr "Choose an {{orgUnitLabel}}" + msgid "Choose a {{categoryName}}" msgstr "Choose a {{categoryName}}" @@ -833,6 +1136,9 @@ msgstr "No programs available." msgid "Search for a program" msgstr "Search for a program" +msgid "Some programs are being filtered by the chosen {{orgUnitLabel}}" +msgstr "Some programs are being filtered by the chosen {{orgUnitLabel}}" + msgid "Show all programs" msgstr "Show all programs" @@ -966,6 +1272,9 @@ msgstr "Create saved list" msgid "Create new in another program..." msgstr "Create new in another program..." +msgid "Create new {{eventLabel}}" +msgstr "Create new {{eventLabel}}" + msgid "Search for a {{trackedEntityName}} in {{programName}}" msgstr "Search for a {{trackedEntityName}} in {{programName}}" @@ -987,24 +1296,53 @@ msgstr "Assigned to" msgid "Edit" msgstr "Edit" +msgid "No one is assigned to this {{eventLabel}}" +msgstr "No one is assigned to this {{eventLabel}}" + msgid "Assign" msgstr "Assign" +msgid "Check for {{enrollmentsLabel}}" +msgstr "Check for {{enrollmentsLabel}}" + msgid "This program is protected" msgstr "This program is protected" +msgid "" +"You must provide a reason to check for {{enrollmentsLabel}} in this " +"protected program." +msgstr "" +"You must provide a reason to check for {{enrollmentsLabel}} in this " +"protected program." + msgid "All activity will be logged." msgstr "All activity will be logged." +msgid "Reason to check for {{enrollmentsLabel}}" +msgstr "Reason to check for {{enrollmentsLabel}}" + +msgid "" +"Describe the reason you are checking for {{enrollmentsLabel}} in this " +"protected program" +msgstr "" +"Describe the reason you are checking for {{enrollmentsLabel}} in this " +"protected program" + msgid "Unsaved changes" msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" +msgid "{{enrollmentLabel}} actions" +msgstr "{{enrollmentLabel}} actions" + msgid "We are processing your request." msgstr "We are processing your request." +msgid "Only one {{enrollmentLabel}} per {{tetName}} is allowed in this program" +msgstr "Only one {{enrollmentLabel}} per {{tetName}} is allowed in this program" + msgid "Add new" msgstr "Add new" @@ -1017,12 +1355,36 @@ msgstr "Mark as cancelled" msgid "Mark incomplete" msgstr "Mark incomplete" +msgid "You do not have access to delete this {{enrollmentLabel}}" +msgstr "You do not have access to delete this {{enrollmentLabel}}" + +msgid "Delete {{enrollmentLabel}}" +msgstr "Delete {{enrollmentLabel}}" + +msgid "Are you sure you want to delete this {{enrollmentLabel}}?" +msgstr "Are you sure you want to delete this {{enrollmentLabel}}?" + +msgid "This will permanently remove the current {{enrollmentLabel}}." +msgstr "This will permanently remove the current {{enrollmentLabel}}." + +msgid "Yes, delete {{enrollmentLabel}}." +msgstr "Yes, delete {{enrollmentLabel}}." + +msgid "Remove mark for {{followUpLabel}}" +msgstr "Remove mark for {{followUpLabel}}" + +msgid "Mark for {{followUpLabel}}" +msgstr "Mark for {{followUpLabel}}" + msgid "Transfer" msgstr "Transfer" msgid "An error occurred while transferring ownership" msgstr "An error occurred while transferring ownership" +msgid "Existing dates for auto-generated {{eventsLabel}} will not be updated." +msgstr "Existing dates for auto-generated {{eventsLabel}} will not be updated." + msgid "Latitude" msgstr "Latitude" @@ -1050,12 +1412,32 @@ msgstr "Finish drawing before saving" msgid "Set area" msgstr "Set area" +msgid "" +"Transferring {{enrollmentLabel}} ownership from {{ownerOrgUnit}} to " +"{{newOrgUnit}}{{escape}}" +msgstr "" +"Transferring {{enrollmentLabel}} ownership from {{ownerOrgUnit}} to " +"{{newOrgUnit}}{{escape}}" + msgid "Transfer Ownership" msgstr "Transfer Ownership" +msgid "" +"Choose the {{orgUnitLabel}} to which {{enrollmentLabel}} ownership should " +"be transferred." +msgstr "" +"Choose the {{orgUnitLabel}} to which {{enrollmentLabel}} ownership should " +"be transferred." + +msgid "{{enrollmentLabel}} date" +msgstr "{{enrollmentLabel}} date" + msgid "Incident date" msgstr "Incident date" +msgid "{{enrollmentLabel}} widget could not be loaded. Please try again later" +msgstr "{{enrollmentLabel}} widget could not be loaded. Please try again later" + msgid "Started at{{escape}}" msgstr "Started at{{escape}}" @@ -1071,18 +1453,57 @@ msgstr "Add coordinates" msgid "Add area" msgstr "Add area" +msgid "{{orgUnitLabel}} could not be retrieved. Please try again later." +msgstr "{{orgUnitLabel}} could not be retrieved. Please try again later." + msgid "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgid "Saving to {{stageName}} for {{programName}}" msgstr "Saving to {{stageName}} for {{programName}}" +msgid "Program or {{programStageLabel}} is invalid" +msgstr "Program or {{programStageLabel}} is invalid" + +msgid "Notes about this {{enrollmentLabel}}" +msgstr "Notes about this {{enrollmentLabel}}" + +msgid "Write a {{noteLabel}} about this {{enrollmentLabel}}" +msgstr "Write a {{noteLabel}} about this {{enrollmentLabel}}" + +msgid "This {{enrollmentLabel}} doesn't have any notes" +msgstr "This {{enrollmentLabel}} doesn't have any notes" + msgid "Error" msgstr "Error" msgid "Warning" msgstr "Warning" +msgid "{{programStageLabel}} not found in rules execution" +msgstr "{{programStageLabel}} not found in rules execution" + +msgid "Delete {{eventLabel}}" +msgstr "Delete {{eventLabel}}" + +msgid "Deleting an {{eventLabel}} is permanent and cannot be undone." +msgstr "Deleting an {{eventLabel}} is permanent and cannot be undone." + +msgid "Are you sure you want to delete this {{eventLabel}}? " +msgstr "Are you sure you want to delete this {{eventLabel}}? " + +msgid "Yes, delete {{eventLabel}}" +msgstr "Yes, delete {{eventLabel}}" + +msgid "Go to “Schedule” tab to reschedule this {{eventLabel}}" +msgstr "Go to “Schedule” tab to reschedule this {{eventLabel}}" + +msgid "Scheduled date cannot be changed for {{ eventStatus }} {{eventsLabel}}" +msgstr "Scheduled date cannot be changed for {{ eventStatus }} {{eventsLabel}}" + +msgid "You do not have access to uncomplete this {{eventLabel}}" +msgstr "You do not have access to uncomplete this {{eventLabel}}" + msgid "Geometry (Area)" msgstr "Geometry (Area)" @@ -1095,6 +1516,15 @@ msgstr "Polygon captured" msgid "No polygon captured" msgstr "No polygon captured" +msgid "{{eventLabel}} completed" +msgstr "{{eventLabel}} completed" + +msgid "Notes about this {{eventLabel}}" +msgstr "Notes about this {{eventLabel}}" + +msgid "Write a {{noteLabel}} about this {{eventLabel}}" +msgstr "Write a {{noteLabel}} about this {{eventLabel}}" + msgid "after" msgstr "after" @@ -1115,15 +1545,34 @@ msgstr[1] "The scheduled date is {{count}} days {{position}} the suggested date. msgid "Schedule date / Due date" msgstr "Schedule date / Due date" +msgid "" +"Scheduling an {{eventLabel}} in {{stageName}} for {{programName}} in " +"{{orgUnitName}}" +msgstr "" +"Scheduling an {{eventLabel}} in {{stageName}} for {{programName}} in " +"{{orgUnitName}}" + +msgid "Scheduling an {{eventLabel}} in {{stageName}} for {{programName}}" +msgstr "Scheduling an {{eventLabel}} in {{stageName}} for {{programName}}" + msgid "Schedule info" msgstr "Schedule info" +msgid "{{eventLabel}} notes" +msgstr "{{eventLabel}} notes" + +msgid "Write a {{noteLabel}} about this scheduled {{eventLabel}}" +msgstr "Write a {{noteLabel}} about this scheduled {{eventLabel}}" + msgid "Feedback" msgstr "Feedback" msgid "Indicators" msgstr "Indicators" +msgid "Save {{noteLabel}}" +msgstr "Save {{noteLabel}}" + msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" @@ -1199,6 +1648,15 @@ msgstr "You do not have access to delete this {{trackedEntityTypeName}}" msgid "Delete {{trackedEntityTypeName}}" msgstr "Delete {{trackedEntityTypeName}}" +msgid "" +"Are you sure you want to delete this {{trackedEntityTypeName}}? This will " +"permanently remove the {{trackedEntityTypeName}} and all its associated " +"{{enrollmentsLabel}} and {{eventsLabel}} in all programs." +msgstr "" +"Are you sure you want to delete this {{trackedEntityTypeName}}? This will " +"permanently remove the {{trackedEntityTypeName}} and all its associated " +"{{enrollmentsLabel}} and {{eventsLabel}} in all programs." + msgid "There was a problem deleting the {{trackedEntityTypeName}}" msgstr "There was a problem deleting the {{trackedEntityTypeName}}" @@ -1211,9 +1669,30 @@ msgstr "View profile" msgid "Profile widget could not be loaded. Please try again later" msgstr "Profile widget could not be loaded. Please try again later" +msgid "No {{attributeLabel}} configured for {{trackedEntityTypeName}}" +msgstr "No {{attributeLabel}} configured for {{trackedEntityTypeName}}" + +msgid "No {{attributeLabel}} configured" +msgstr "No {{attributeLabel}} configured" + msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" +msgid "Choose a {{linkableStageLabel}} {{eventLabel}}" +msgstr "Choose a {{linkableStageLabel}} {{eventLabel}}" + +msgid "Select an {{eventLabel}}" +msgstr "Select an {{eventLabel}}" + +msgid "{{ linkableStageLabel }} can only have one {{eventLabel}}" +msgstr "{{ linkableStageLabel }} can only have one {{eventLabel}}" + +msgid "{{ linkableStageLabel }} has no linkable {{eventsLabel}}" +msgstr "{{ linkableStageLabel }} has no linkable {{eventsLabel}}" + +msgid "Link to an existing {{eventLabel}}" +msgstr "Link to an existing {{eventLabel}}" + msgid "Actions - {{relationshipName}}" msgstr "Actions - {{relationshipName}}" @@ -1223,9 +1702,18 @@ msgstr "Ambiguous relationships, contact system administrator" msgid "Enter details" msgstr "Enter details" +msgid "Linked {{eventLabel}}" +msgstr "Linked {{eventLabel}}" + msgid "Enter details now" msgstr "Enter details now" +msgid "The {{eventLabel}} was successfully linked" +msgstr "The {{eventLabel}} was successfully linked" + +msgid "An error occurred while linking the {{eventLabel}}" +msgstr "An error occurred while linking the {{eventLabel}}" + msgid "Scheduled date" msgstr "Scheduled date" @@ -1235,24 +1723,116 @@ msgstr "Report date" msgid "Please enter a date" msgstr "Please enter a date" +msgid "Please select a valid {{eventLabel}}" +msgstr "Please select a valid {{eventLabel}}" + +msgid "This {{programStageLabel}} can only have one {{eventLabel}}" +msgstr "This {{programStageLabel}} can only have one {{eventLabel}}" + +msgid "New {{ eventName }} {{eventLabel}}" +msgstr "New {{ eventName }} {{eventLabel}}" + +msgid "" +"{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be " +"deleted" +msgstr "" +"{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be " +"deleted" + +msgid "This {{eventLabel}} is outside the edit period" +msgstr "This {{eventLabel}} is outside the edit period" + +msgid "An error occurred while deleting the {{eventLabel}}" +msgstr "An error occurred while deleting the {{eventLabel}}" + +msgid "Are you sure you want to delete this {{eventLabel}}?" +msgstr "Are you sure you want to delete this {{eventLabel}}?" + +msgid "An error occurred when updating {{eventLabel}} status" +msgstr "An error occurred when updating {{eventLabel}} status" + msgid "Unskip" msgstr "Unskip" msgid "Skip" msgstr "Skip" +msgid "To open this {{eventLabel}}, please wait until saving is complete" +msgstr "To open this {{eventLabel}}, please wait until saving is complete" + msgid "Show {{ rest }} more" msgstr "Show {{ rest }} more" msgid "Go to full {{ eventName }}" msgstr "Go to full {{ eventName }}" +msgid "{{eventsLabel}} could not be retrieved. Please try again later." +msgstr "{{eventsLabel}} could not be retrieved. Please try again later." + msgid "{{ overdueEvents }} overdue" msgstr "{{ overdueEvents }} overdue" msgid "{{ scheduledEvents }} scheduled" msgstr "{{ scheduledEvents }} scheduled" +msgid "No {{programStagesLabel}} found in this program" +msgstr "No {{programStagesLabel}} found in this program" + +msgid "{{programStagesLabel}} and {{eventsLabel}}" +msgstr "{{programStagesLabel}} and {{eventsLabel}}" + +msgid "An error occurred while unlinking and deleting the {{eventLabel}}." +msgstr "An error occurred while unlinking and deleting the {{eventLabel}}." + +msgid "Unlink and delete linked {{eventLabel}}" +msgstr "Unlink and delete linked {{eventLabel}}" + +msgid "" +"Are you sure you want to remove the link and delete the linked " +"{{eventLabel}}?" +msgstr "" +"Are you sure you want to remove the link and delete the linked " +"{{eventLabel}}?" + +msgid "" +"This action permanently removes the link, linked {{eventLabel}}, and all " +"related data." +msgstr "" +"This action permanently removes the link, linked {{eventLabel}}, and all " +"related data." + +msgid "Yes, unlink and delete linked {{eventLabel}}" +msgstr "Yes, unlink and delete linked {{eventLabel}}" + +msgid "Unlink {{eventLabel}}" +msgstr "Unlink {{eventLabel}}" + +msgid "Are you sure you want to remove the link between these {{eventsLabel}}?" +msgstr "Are you sure you want to remove the link between these {{eventsLabel}}?" + +msgid "" +"This action removes the link itself, but the linked {{eventLabel}} will " +"remain." +msgstr "" +"This action removes the link itself, but the linked {{eventLabel}} will " +"remain." + +msgid "Yes, unlink {{eventLabel}}" +msgstr "Yes, unlink {{eventLabel}}" + +msgid "View linked {{eventLabel}}" +msgstr "View linked {{eventLabel}}" + +msgid "You do not have access to remove the link between these {{eventsLabel}}" +msgstr "You do not have access to remove the link between these {{eventsLabel}}" + +msgid "" +"You do not have access to remove the link and delete the linked " +"{{eventLabel}}" +msgstr "" +"You do not have access to remove the link and delete the linked " +"{{eventLabel}}" + msgid "An error occurred while loading the widget." msgstr "An error occurred while loading the widget." @@ -1307,15 +1887,55 @@ msgstr "New {{trackedEntityTypeName}} relationship" msgid "Missing implementation step" msgstr "Missing implementation step" +msgid "Go back without saving {{relationshipLabel}}" +msgstr "Go back without saving {{relationshipLabel}}" + +msgid "New {{relationshipLabel}}" +msgstr "New {{relationshipLabel}}" + msgid "Link to an existing {{tetName}}" msgstr "Link to an existing {{tetName}}" +msgid "An error occurred while adding the {{relationshipLabel}}" +msgstr "An error occurred while adding the {{relationshipLabel}}" + +msgid "" +"Something went wrong while loading {{relationshipLabel}}. Please try again " +"later." +msgstr "" +"Something went wrong while loading {{relationshipLabel}}. Please try again " +"later." + +msgid "{{trackedEntityTypeName}} {{relationshipLabel}}" +msgstr "{{trackedEntityTypeName}} {{relationshipLabel}}" + +msgid "Delete {{relationshipLabel}}" +msgstr "Delete {{relationshipLabel}}" + +msgid "Deleting the {{relationshipLabel}} is permanent and cannot be undone." +msgstr "Deleting the {{relationshipLabel}} is permanent and cannot be undone." + +msgid "Are you sure you want to delete this {{relationshipLabel}}?" +msgstr "Are you sure you want to delete this {{relationshipLabel}}?" + +msgid "Yes, delete {{relationshipLabel}}" +msgstr "Yes, delete {{relationshipLabel}}" + +msgid "An error occurred while deleting the {{relationshipLabel}}." +msgstr "An error occurred while deleting the {{relationshipLabel}}." + +msgid "This {{enrollmentLabel}} doesn't have any relationships" +msgstr "This {{enrollmentLabel}} doesn't have any relationships" + msgid "Type" msgstr "Type" msgid "Created date" msgstr "Created date" +msgid "{{programStageLabel}} name" +msgstr "{{programStageLabel}} name" + msgid "Working list could not be loaded" msgstr "Working list could not be loaded" @@ -1325,26 +1945,199 @@ msgstr "Download data..." msgid "an error occurred loading working lists" msgstr "an error occurred loading working lists" +msgid "You do not have access to complete {{eventsLabel}}" +msgstr "You do not have access to complete {{eventsLabel}}" + msgid "There is a bulk data entry with unsaved changes" msgstr "There is a bulk data entry with unsaved changes" +msgid "Complete {{eventsLabel}}" +msgstr "Complete {{eventsLabel}}" + +msgid "Are you sure you want to complete all active {{eventsLabel}} in selection?" +msgstr "Are you sure you want to complete all active {{eventsLabel}} in selection?" + +msgid "There are no active {{eventsLabel}} to complete in the current selection." +msgstr "There are no active {{eventsLabel}} to complete in the current selection." + +msgid "Error completing {{eventsLabel}}" +msgstr "Error completing {{eventsLabel}}" + +msgid "There was an error completing the {{eventsLabel}}." +msgstr "There was an error completing the {{eventsLabel}}." + msgid "Details (Advanced)" msgstr "Details (Advanced)" msgid "An unknown error occurred." msgstr "An unknown error occurred." +msgid "An error occurred while completing {{eventsLabel}}" +msgstr "An error occurred while completing {{eventsLabel}}" + +msgid "You do not have access to delete {{eventsLabel}}" +msgstr "You do not have access to delete {{eventsLabel}}" + +msgid "An error occurred while deleting the {{eventsLabel}}" +msgstr "An error occurred while deleting the {{eventsLabel}}" + +msgid "Delete {{eventsLabel}}" +msgstr "Delete {{eventsLabel}}" + msgid "This cannot be undone." msgstr "This cannot be undone." +msgid "Are you sure you want to delete the selected {{eventsLabel}}?" +msgstr "Are you sure you want to delete the selected {{eventsLabel}}?" + +msgid "Owner {{orgUnitLabel}}" +msgstr "Owner {{orgUnitLabel}}" + msgid "Registration Date" msgstr "Registration Date" +msgid "{{eventLabel}} {{orgUnitLabel}}" +msgstr "{{eventLabel}} {{orgUnitLabel}}" + +msgid "{{enrollmentLabel}} status" +msgstr "{{enrollmentLabel}} status" + +msgid "Choose a {{programStageLabel}} to filter by {{label}}" +msgstr "Choose a {{programStageLabel}} to filter by {{label}}" + +msgid "You do not have access to bulk complete {{enrollmentsLabel}}" +msgstr "You do not have access to bulk complete {{enrollmentsLabel}}" + +msgid "" +"Some {{enrollmentsLabel}} were completed successfully, but there was an " +"error while completing the rest. Please see the details below." +msgstr "" +"Some {{enrollmentsLabel}} were completed successfully, but there was an " +"error while completing the rest. Please see the details below." + +msgid "" +"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " +"Please try again." +msgstr "" +"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " +"Please try again." + +msgid "There are currently no active {{enrollmentsLabel}} in the selection." +msgstr "There are currently no active {{enrollmentsLabel}} in the selection." + +msgid "All {{enrollmentsLabel}} are already completed or cancelled." +msgstr "All {{enrollmentsLabel}} are already completed or cancelled." + +msgid "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgid_plural "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgstr[0] "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgstr[1] "" +"This action will complete {{count}} active {{enrollmentsLabel}} in your " +"selection." + +msgid "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgid_plural "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgstr[0] "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgstr[1] "" +"{{count}} {{enrollmentsLabel}} already marked as completed will not be " +"changed." + +msgid "Mark all {{eventsLabel}} within {{enrollmentsLabel}} as complete" +msgstr "Mark all {{eventsLabel}} within {{enrollmentsLabel}} as complete" + +msgid "Complete {{enrollmentsLabel}}" +msgstr "Complete {{enrollmentsLabel}}" + +msgid "Error completing {{enrollmentsLabel}}" +msgstr "Error completing {{enrollmentsLabel}}" + +msgid "No active {{enrollmentsLabel}} to complete" +msgstr "No active {{enrollmentsLabel}} to complete" + +msgid "Complete {{count}} {{enrollmentLabel}}" +msgid_plural "Complete {{count}} {{enrollmentLabel}}" +msgstr[0] "Complete {{count}} {{enrollmentLabel}}" +msgstr[1] "Complete {{count}} {{enrollmentsLabel}}" + +msgid "An error occurred when completing the {{enrollmentsLabel}}" +msgstr "An error occurred when completing the {{enrollmentsLabel}}" + +msgid "An unknown error occurred when completing {{enrollmentsLabel}}" +msgstr "An unknown error occurred when completing {{enrollmentsLabel}}" + +msgid "You do not have access to delete {{enrollmentsLabel}}" +msgstr "You do not have access to delete {{enrollmentsLabel}}" + +msgid "Delete {{enrollmentsLabel}}" +msgstr "Delete {{enrollmentsLabel}}" + +msgid "Delete selected {{enrollmentsLabel}}" +msgstr "Delete selected {{enrollmentsLabel}}" + +msgid "" +"An error occurred while loading the selected {{enrollmentsLabel}}. Please " +"try again." +msgstr "" +"An error occurred while loading the selected {{enrollmentsLabel}}. Please " +"try again." + +msgid "" +"This action will permanently delete the selected {{enrollmentsLabel}}, " +"including all associated data and {{eventsLabel}}." +msgstr "" +"This action will permanently delete the selected {{enrollmentsLabel}}, " +"including all associated data and {{eventsLabel}}." + +msgid "Active {{enrollmentsLabel}} ({{count}})" +msgid_plural "Active {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Active {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Active {{enrollmentsLabel}} ({{count}})" + +msgid "Completed {{enrollmentsLabel}} ({{count}})" +msgid_plural "Completed {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Completed {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Completed {{enrollmentsLabel}} ({{count}})" + +msgid "Cancelled {{enrollmentsLabel}} ({{count}})" +msgid_plural "Cancelled {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Cancelled {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Cancelled {{enrollmentsLabel}} ({{count}})" + +msgid "Delete {{count}} {{enrollmentLabel}}" +msgid_plural "Delete {{count}} {{enrollmentLabel}}" +msgstr[0] "Delete {{count}} {{enrollmentLabel}}" +msgstr[1] "Delete {{count}} {{enrollmentsLabel}}" + +msgid "An error occurred when deleting {{enrollmentsLabel}}" +msgstr "An error occurred when deleting {{enrollmentsLabel}}" + +msgid "Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}" +msgstr "Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}" + msgid "Delete {{count}} {{ trackedEntityName }}" msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" msgstr[1] "Delete {{count}} {{ trackedEntityName }}" +msgid "" +"Deleting records will also delete any associated {{enrollmentsLabel}} and " +"{{eventsLabel}}." +msgstr "" +"Deleting records will also delete any associated {{enrollmentsLabel}} and " +"{{eventsLabel}}." + msgid "Are you sure you want to delete?" msgstr "Are you sure you want to delete?" @@ -1485,6 +2278,12 @@ msgstr "Program not found" msgid "Program is not a tracker program" msgstr "Program is not a tracker program" +msgid "Could not save {{eventLabel}}" +msgstr "Could not save {{eventLabel}}" + +msgid "Could not delete {{eventLabel}}" +msgstr "Could not delete {{eventLabel}}" + msgid "Could not save working list" msgstr "Could not save working list" @@ -1500,9 +2299,27 @@ msgstr "Organisation unit search failed." msgid "Error saving tracked entity instance" msgstr "Error saving tracked entity instance" +msgid "Error saving {{enrollmentLabel}}" +msgstr "Error saving {{enrollmentLabel}}" + +msgid "Error saving the {{enrollmentLabel}} {{eventLabel}}" +msgstr "Error saving the {{enrollmentLabel}} {{eventLabel}}" + +msgid "Error deleting the {{enrollmentLabel}} {{eventLabel}}" +msgstr "Error deleting the {{enrollmentLabel}} {{eventLabel}}" + +msgid "Error editing the {{eventLabel}}, the changes made were not saved" +msgstr "Error editing the {{eventLabel}}, the changes made were not saved" + msgid "Error updating the Assignee" msgstr "Error updating the Assignee" +msgid "Could not save {{enrollmentLabel}} {{noteLabel}}" +msgstr "Could not save {{enrollmentLabel}} {{noteLabel}}" + +msgid "Could not save {{eventLabel}} {{noteLabel}}" +msgstr "Could not save {{eventLabel}} {{noteLabel}}" + msgid "There was an error fetching metadata" msgstr "There was an error fetching metadata" @@ -1552,6 +2369,22 @@ msgstr "Please enter a valid time" msgid "Please enter a time" msgstr "Please enter a time" +msgid "" +"The following {{attributeLabel}} type is not supported for searching and " +"has been hidden" +msgid_plural "" +"The following {{attributeLabel}} type is not supported for searching and " +"has been hidden" +msgstr[0] "" +"The following {{attributeLabel}} type is not supported for searching and " +"has been hidden" +msgstr[1] "" +"The following {{attributeLabel}} types are not supported for searching and " +"have been hidden" + +msgid "Some {{attributeLabel}} are hidden" +msgstr "Some {{attributeLabel}} are hidden" + msgid "Set coordinate" msgstr "Set coordinate" diff --git a/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts index c709201965..96dbb7c8f8 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/BulkDataEntryBreadcrumb/hooks/useOriginLabel.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { breadcrumbsKeys } from '../BulkDataEntryBreadcrumb'; import { useTermLabel } from '../../../../metaData'; -import { tCustomTerm } from '../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../utils/customTerms'; type Props = { programId: string; @@ -36,9 +36,9 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) const defaultFilterLabels = useMemo(() => ({ default: i18n.t('Program overview'), - active: tCustomTerm('Active {{enrollmentsLabel}}', { enrollmentsLabel }), - complete: tCustomTerm('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), - cancelled: tCustomTerm('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + active: customTerms.i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + complete: customTerms.i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + cancelled: customTerms.i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), }), [enrollmentsLabel]); const label = useMemo(() => { diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx index e275a0085a..6ac6126e96 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState, ComponentType } from 'react'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { colors } from '@dhis2/ui'; import { useTermLabel } from '../../../metaData'; -import { tCustomTerm } from '../../../utils/tCustomTerm'; +import { customTerms } from '../../../utils/customTerms'; import { DirectionalChevron } from '../../../utils/rtl'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; import { BreadcrumbItem } from '../common/BreadcrumbItem'; @@ -104,14 +104,14 @@ const BreadcrumbsPlain = ({ { key: pageKeys.OVERVIEW, onClick: () => handleNavigation(onBackToDashboard, pageKeys.OVERVIEW), - label: tCustomTerm('{{enrollmentLabel}} dashboard', { enrollmentLabel }), + label: customTerms.i18n.t('{{enrollmentLabel}} dashboard', { enrollmentLabel }), selected: page === pageKeys.OVERVIEW, condition: true, }, { key: pageKeys.VIEW_EVENT, onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT), - label: tCustomTerm('View {{eventLabel}}', { eventLabel }), + label: customTerms.i18n.t('View {{eventLabel}}', { eventLabel }), selected: page === pageKeys.VIEW_EVENT, condition: page === pageKeys.VIEW_EVENT || (page === pageKeys.EDIT_EVENT && !eventIsScheduled(eventStatus)), @@ -119,14 +119,14 @@ const BreadcrumbsPlain = ({ { key: pageKeys.EDIT_EVENT, onClick: () => undefined, - label: tCustomTerm('Edit {{eventLabel}}', { eventLabel }), + label: customTerms.i18n.t('Edit {{eventLabel}}', { eventLabel }), selected: page === pageKeys.EDIT_EVENT, condition: page === pageKeys.EDIT_EVENT, }, { key: pageKeys.NEW_EVENT, onClick: () => undefined, - label: tCustomTerm('New {{eventLabel}}', { eventLabel }), + label: customTerms.i18n.t('New {{eventLabel}}', { eventLabel }), selected: page === pageKeys.NEW_EVENT, condition: page === pageKeys.NEW_EVENT, }, diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts index 37c1531009..071b13a438 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/hooks/useWorkingListLabel.ts @@ -2,7 +2,7 @@ import i18n from '@dhis2/d2-i18n'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useTermLabel } from '../../../../metaData'; -import { tCustomTerm } from '../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../utils/customTerms'; type Template = { id: string; @@ -39,9 +39,9 @@ export const useWorkingListLabel = ({ const defaultFilterLabels: { [key in DefaultFilterKey]: string } = useMemo(() => ({ [DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'), - [DefaultFilterKeys.ACTIVE]: tCustomTerm('Active {{enrollmentsLabel}}', { enrollmentsLabel }), - [DefaultFilterKeys.COMPLETE]: tCustomTerm('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), - [DefaultFilterKeys.CANCELLED]: tCustomTerm('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.ACTIVE]: customTerms.i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.COMPLETE]: customTerms.i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.CANCELLED]: customTerms.i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), }), [enrollmentsLabel]); const label: string = useMemo(() => { diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx index 949bcd4d51..e69729defa 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx @@ -7,7 +7,7 @@ import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; import { useTermLabel } from '../../../metaData'; -import { tCustomTerm } from '../../../utils/tCustomTerm'; +import { customTerms } from '../../../utils/customTerms'; export const pageKeys = { MAIN_PAGE: 'mainPage', @@ -73,14 +73,14 @@ const EventBreadcrumbPlain = ({ { key: pageKeys.VIEW_EVENT, onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT), - label: tCustomTerm('View {{eventLabel}}', { eventLabel }), + label: customTerms.i18n.t('View {{eventLabel}}', { eventLabel }), selected: page === pageKeys.VIEW_EVENT, condition: page === pageKeys.VIEW_EVENT || page === pageKeys.EDIT_EVENT, }, { key: pageKeys.EDIT_EVENT, onClick: () => undefined, - label: tCustomTerm('Edit {{eventLabel}}', { eventLabel }), + label: customTerms.i18n.t('Edit {{eventLabel}}', { eventLabel }), selected: page === pageKeys.EDIT_EVENT, condition: page === pageKeys.EDIT_EVENT, }, diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts index 72722ccaa5..29b333d5bb 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts +++ b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useSelector } from 'react-redux'; import { useTermLabel } from '../../../../metaData'; -import { tCustomTerm } from '../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../utils/customTerms'; type Template = { id: string; @@ -36,7 +36,7 @@ export const useWorkingListLabel = ({ programId }: Props) => { return selectedTemplete.name; } - return tCustomTerm('{{eventLabel}} list', { eventLabel }); + return customTerms.i18n.t('{{eventLabel}} list', { eventLabel }); }, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete, eventLabel]); return { diff --git a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx index ba8b6de8c5..80979edca0 100644 --- a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx @@ -9,7 +9,7 @@ import { navigateToEnrollmentOverview, } from '../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; import { useTermLabel } from '../../metaData'; -import { tCustomTerm } from '../../utils/tCustomTerm'; +import { customTerms } from '../../utils/customTerms'; type Props = { currentSearchScopeId?: string, @@ -118,7 +118,7 @@ const CardListButtons: FC = ({ { dataTest: 'view-active-enrollment-button', onClick: onHandleClick, - label: tCustomTerm('View active {{enrollmentLabel}}', { enrollmentLabel }), + label: customTerms.i18n.t('View active {{enrollmentLabel}}', { enrollmentLabel }), hide: navigationButtonsState !== availableCardListButtonState.SHOW_VIEW_ACTIVE_ENROLLMENT_BUTTON, }, { diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx index 0108d6d6c1..071f5dad8e 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx @@ -21,7 +21,7 @@ import { useTermLabel, } from '../../metaData'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; -import { tCustomTerm } from '../../utils/tCustomTerm'; +import { customTerms } from '../../utils/customTerms'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; type OwnProps = { @@ -228,7 +228,7 @@ const CardListItemIndex = ({ { isApplicable: (props: any) => props.firstStageMetaData && props.firstStageMetaData.stage?.stageForm, getComponent: () => completeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: tCustomTerm('Complete {{eventLabel}}', { + label: customTerms.i18n.t('Complete {{eventLabel}}', { eventLabel: getTermLabel('event', { programId: props.programId }), }), id: 'complete', diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx index 2940f3b7a1..671c390912 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx @@ -5,7 +5,7 @@ import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { compose } from 'redux'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { scopeTypes, useTermLabel } from '../../../metaData'; -import { tCustomTerm } from '../../../utils/tCustomTerm'; +import { customTerms } from '../../../utils/customTerms'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { EnrollmentDataEntry } from '../Enrollment'; import type { Props, PlainProps } from './EnrollmentRegistrationEntry.types'; @@ -31,7 +31,7 @@ const translatedTextWithStylesForProgram = ( teiId?: string, ) => ( teiId ? - {tCustomTerm('Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}.', { + {customTerms.i18n.t('Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}.', { enrollmentLabel, programName, orgUnitName, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index a83995e0d3..ef48f496f8 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -16,7 +16,7 @@ import { getEventDateValidatorContainers, getOrgUnitValidatorContainers } from ' import { type RenderFoundation } from '../../../../../metaData'; import { withMainButton } from './withMainButton'; import { getNoteValidatorContainers } from './fieldValidators/note.validatorContainersGetter'; -import { tCustomTerm } from '../../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../../utils/customTerms'; import { withSaveHandler, placements, @@ -329,7 +329,7 @@ const buildCompleteFieldSettingsFn = () => { const completeSettings = { getComponent: () => completeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: tCustomTerm('Complete {{eventLabel}}', { eventLabel: props.eventLabel }), + label: customTerms.i18n.t('Complete {{eventLabel}}', { eventLabel: props.eventLabel }), id: 'complete', }), getPropName: () => 'complete', diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts index 8c457906bd..7b8efff131 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts @@ -4,7 +4,7 @@ import { map } from 'rxjs/operators'; import { batchActions } from 'redux-batched-actions'; import type { EpicAction, ReduxStore } from 'capture-core-utils/types'; import { getTermLabel } from '../../../../../../metaData'; -import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../../../utils/customTerms'; import { initializeNewRelationship, @@ -82,7 +82,7 @@ export const addRelationshipForNewSingleEventEpic = (action$: EpicAction !value; export const getNoteValidatorContainers = (eventLabel: string, noteLabel: string) => [ { validator: validateNote, - errorMessage: tCustomTerm('Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}', { + errorMessage: customTerms.i18n.t('Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}', { eventLabel, noteLabel, }), diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts index b8c2041b54..462de78ee5 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts @@ -1,11 +1,11 @@ import { isValidOrgUnit } from 'capture-core-utils/validators/form'; -import { tCustomTerm } from '../../../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../../../utils/customTerms'; const validateOrgUnit = (value?: any) => isValidOrgUnit(value); export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ { validator: validateOrgUnit, - errorMessage: tCustomTerm('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + errorMessage: customTerms.i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), }, ]; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx index a2f1b8a33b..15d215f2c7 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx @@ -6,7 +6,7 @@ import { OfflineEventsList } from '../../../../EventsList/OfflineEventsList/Offl import { listId } from './RecentlyAddedEventsList.const'; import type { Props } from './RecentlyAddedEventsList.types'; import { useTermLabel } from '../../../../../metaData'; -import { tCustomTerm } from '../../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../../utils/customTerms'; const styles = (theme: any) => ({ container: { @@ -30,7 +30,7 @@ const NewEventsListPlain = (props: Props & WithStyles) => {
- {tCustomTerm('{{count}} {{eventLabel}} added', { + {customTerms.i18n.t('{{count}} {{eventLabel}} added', { count: eventsAdded, eventLabel, eventsLabel, @@ -40,8 +40,8 @@ const NewEventsListPlain = (props: Props & WithStyles) => {
diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx index 412e02151c..85ff2549ca 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx @@ -6,7 +6,7 @@ import { NewRelationship } from '../../../Pages/NewRelationship/NewRelationship. import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { LinkButton } from '../../../Buttons/LinkButton.component'; import { getTermLabel } from '../../../../metaData'; -import { tCustomTerm } from '../../../../utils/tCustomTerm'; +import { customTerms } from '../../../../utils/customTerms'; const getStyles = (theme: any) => ({ headerContainer: { @@ -90,13 +90,16 @@ class NewEventNewRelationshipWrapper extends React.Component
- {tCustomTerm('Adding {{relationshipLabel}} to {{eventLabel}}.', { eventLabel, relationshipLabel })} + {customTerms.i18n.t( + 'Adding {{relationshipLabel}} to {{eventLabel}}.', + { eventLabel, relationshipLabel }, + )} - {tCustomTerm( + {customTerms.i18n.t( 'Go back to {{eventLabel}} without saving {{relationshipLabel}}', { eventLabel, relationshipLabel }, )} @@ -104,7 +107,10 @@ class NewEventNewRelationshipWrapper extends React.Component { const dispatch = useDispatch(); @@ -15,7 +15,7 @@ export const SingleEventRegistrationEntryComponent = ({ showAddRelationship, eve if (!eventAccess.write) { return ( -

{tCustomTerm( +

{customTerms.i18n.t( 'Would you like to complete the {{enrollmentLabel}} and all active {{eventsLabel}} as well?', { enrollmentLabel, eventsLabel }, )}

{Object.keys(programStagesWithActiveEvents).length !== 0 && ( <> - {tCustomTerm('The following {{eventsLabel}} will be completed:', { eventsLabel })} + {customTerms.i18n.t('The following {{eventsLabel}} will be completed:', { eventsLabel })} {Object.keys(programStagesWithActiveEvents).map((key) => { const { count, name } = programStagesWithActiveEvents[key]; return (