diff --git a/i18n/en.pot b/i18n/en.pot index cb89718162..c79ff5717a 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-17T06:08:25.172Z\n" -"PO-Revision-Date: 2026-08-17T06:08:25.172Z\n" +"POT-Creation-Date: 2026-08-17T07:54:36.289Z\n" +"PO-Revision-Date: 2026-08-17T07:54:36.289Z\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." @@ -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" @@ -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" @@ -1467,9 +1464,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 +1473,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 +1491,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 +1587,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 +1745,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 +1795,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 +2239,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-utils/featuresSupport/support.ts b/src/core_modules/capture-core-utils/featuresSupport/support.ts index baeb663dae..90274517ae 100644 --- a/src/core_modules/capture-core-utils/featuresSupport/support.ts +++ b/src/core_modules/capture-core-utils/featuresSupport/support.ts @@ -6,6 +6,7 @@ export const FEATURES = Object.freeze({ orgUnitReplaceOuQueryParam: 'orgUnitReplaceOuQueryParam', enrollmentStatusReplaceProgramStatusQueryParam: 'enrollmentStatusReplaceProgramStatusQueryParam', emptyValueFilter: 'emptyValueFilter', + customTerminologyPlurals: 'customTerminologyPlurals', }); const MINOR_VERSION_SUPPORT = Object.freeze({ @@ -16,6 +17,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/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/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/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/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/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/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/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index 048ac5018c..b76d2e87ce 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,26 @@ 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,displayProgramStagesLabel,displayEventsLabel', +]; + export const useProgram = (programId: string) => { const { error, loading, data } = useDataQuery( useMemo( @@ -13,15 +28,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, }, }, }), 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 f01c953420..026a42ccdf 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 @@ -145,7 +145,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/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/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/WidgetProfile/hooks/useApiProgram.ts b/src/core_modules/capture-core/components/WidgetProfile/hooks/useApiProgram.ts index ada6c9dde0..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,7 +1,12 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; -const fields = +const trackedEntityTypeFields = + 'id,access,displayName,allowAuditLog,minAttributesRequiredToSearch,featureType,' + + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + + 'translations[property,locale,value]'; + +const buildFields = (): string => 'id,version,displayName,displayShortName,description,programType,style,minAttributesRequiredToSearch,' + 'enrollmentDateLabel,incidentDateLabel,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture,' + 'displayIncidentDate,access[*],' + @@ -23,9 +28,7 @@ const fields = '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]],' + + `trackedEntityType[${trackedEntityTypeFields}],` + 'userRoles[id,displayName]'; export const useApiProgram = (programId: string) => { @@ -36,7 +39,7 @@ export const useApiProgram = (programId: string) => { resource: 'programs', id: programId, params: { - fields, + fields: buildFields(), }, }, }), 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/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/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..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('Stages and Events')} + {i18n.t('Program stages and events')} {showWidgetBadge && (
{ + 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/metaData/TrackedEntityType/TrackedEntityType.ts b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts index 8457d90378..c037bb2ad1 100644 --- a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts +++ b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts @@ -6,7 +6,6 @@ import type { SearchGroup } from '../SearchGroup'; import type { DataElement } from '../DataElement'; import type { TeiRegistration } from './TeiRegistration'; import type { Access } from '../Access'; -import type { CustomLabels } from '../helpers/customLabels'; export class TrackedEntityType { _id!: string; @@ -15,11 +14,9 @@ export class TrackedEntityType { _teiRegistration!: TeiRegistration; _attributes!: Array; _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/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts index 18938840cd..bf60ed5c72 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -5,14 +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' }, - trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel' }, + orgUnit: { field: 'displayOrgUnitLabel' }, + followUp: { field: 'displayFollowUpLabel' }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; @@ -30,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; }; @@ -44,30 +41,11 @@ 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); + const pick = (field?: string) => (field ? list.find(s => s?.[field])?.[field] : undefined); + if (plural && term.pluralField) { + return pick(term.pluralField); } 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); - -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 49b34132fe..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,10 +1,5 @@ -export { - CUSTOM_LABEL_FIELDS, - resolveLabel, - extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, -} from './customLabels'; +export { CUSTOM_LABEL_FIELDS, extractCustomLabels, resolveLabel } from './customLabels'; export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; -export { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } from './useLabel'; +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 c733c2e662..dae529d57f 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -1,45 +1,58 @@ +import i18n from '@dhis2/d2-i18n'; import { useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; +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 }; -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], - ); +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 useStageLabel = ( +type TermLabelOptions = LabelOptions & { stageId?: string }; + +const resolve = ( + programId: string | undefined, 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]); + { 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](); }; -export const useTrackedEntityTypeLabel = ( +/** + * 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, - { tetId, plural }: TrackedEntityTypeOptions = {}, -): string | undefined => { - const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); - const id = tetId ?? currentTetId; + options: TermLabelOptions & { programId?: string } = {}, +): string => { + const { programId, stageId, plural } = options; + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const id = programId ?? currentProgramId; return useMemo( - () => resolveLabel(id ? trackedEntityTypesCollection.get(id)?.customLabels : undefined, key, { plural }), - [id, key, plural], + () => 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 627adbd3b3..94a88e45ee 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -19,13 +19,11 @@ export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { CUSTOM_LABEL_FIELDS, - resolveLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, - useProgramLabel, - useStageLabel, - useTrackedEntityTypeLabel, + 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 00e7aca7aa..a098e321b4 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -41,13 +41,11 @@ export { getProgramAndStageForEventProgram, getEventProgramEventAccess, CUSTOM_LABEL_FIELDS, - resolveLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, - useProgramLabel, - useStageLabel, - useTrackedEntityTypeLabel, + 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 112aa41d14..5c56c02152 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 @@ -239,8 +239,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/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index 5513dce0ee..fb8c442ede 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,7 +81,6 @@ export class TrackedEntityTypeFactory { o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - 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 929645433c..7d972501b6 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'; @@ -97,7 +98,7 @@ const programTrackedEntityAttributeFields = [ 'allowFutureDate', ].join(','); -const programStageFields = [ +const baseProgramStageFields = [ 'id', 'access', 'autoGenerateEvent', @@ -117,7 +118,6 @@ const programStageFields = [ 'displayDueDateLabel', 'displayProgramStageLabel', 'displayEventLabel', - 'displayEventsLabel', 'formType', 'featureType', 'validationStrategy', @@ -126,9 +126,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,16 +142,13 @@ const fieldsParam = [ 'displayIncidentDateLabel', 'displayEnrollmentDateLabel', 'displayEnrollmentLabel', - 'displayEnrollmentsLabel', 'displayFollowUpLabel', 'displayOrgUnitLabel', 'displayRelationshipLabel', 'displayNoteLabel', 'displayTrackedEntityAttributeLabel', 'displayProgramStageLabel', - 'displayProgramStagesLabel', 'displayEventLabel', - 'displayEventsLabel', 'minAttributesRequiredToSearch', 'useFirstStageDuringRegistration', 'onlyEnrollOnce', @@ -163,16 +164,35 @@ 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', + '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 includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); 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..4271797227 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts @@ -26,7 +26,8 @@ const convert = (() => { })); })(); -const fieldsParam = 'id,access,displayName,displayTrackedEntityTypesLabel,minAttributesRequiredToSearch,featureType,' + +const FIELDS = + 'id,access,displayName,minAttributesRequiredToSearch,featureType,' + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + 'translations[property,locale,value]'; @@ -34,7 +35,7 @@ export const storeTrackedEntityTypes = (ids: Array) => { const query = { resource: 'trackedEntityTypes', params: { - fields: fieldsParam, + fields: FIELDS, filter: `id:in:[${ids.join(',')}]`, pageSize: ids.length, }, 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) 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); + } +};