Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
53 changes: 40 additions & 13 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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:32:18.822Z\n"
"PO-Revision-Date: 2026-09-01T14:32:18.822Z\n"
"POT-Creation-Date: 2026-09-09T08:05:05.488Z\n"
"PO-Revision-Date: 2026-09-09T08:05:05.489Z\n"

msgid "The application could not be loaded."
msgstr "The application could not be loaded."
Expand Down Expand Up @@ -424,11 +424,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 "
Expand Down Expand Up @@ -718,6 +718,12 @@ msgstr "Notice"
msgid "Close the notice"
msgstr "Close the notice"

msgid "No feedback for this enrollment yet"
msgstr "No feedback for this enrollment yet"

msgid "No indicator output for this enrollment yet"
msgstr "No indicator output for this enrollment yet"

msgid "Quick actions"
msgstr "Quick actions"

Expand Down Expand Up @@ -1449,9 +1455,6 @@ 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"

msgid "Started at{{escape}}"
msgstr "Started at{{escape}}"

Expand Down Expand Up @@ -1533,8 +1536,8 @@ msgstr "Polygon captured"
msgid "No polygon captured"
msgstr "No polygon captured"

msgid "Event completed"
msgstr "Event completed"
msgid "{{eventLabel}} completed"
msgstr "{{eventLabel}} completed"

msgid "Notes about this event"
msgstr "Notes about this event"
Expand Down Expand Up @@ -1807,8 +1810,8 @@ msgstr "{{ scheduledEvents }} scheduled"
msgid "No stages found in this program"
msgstr "No stages found in this program"

msgid "Stages and Events"
msgstr "Stages and Events"
msgid "{{programStagesLabel}} and {{eventsLabel}}"
msgstr "{{programStagesLabel}} and {{eventsLabel}}"

msgid "An error occurred while unlinking and deleting the event."
msgstr "An error occurred while unlinking and deleting the event."
Expand Down Expand Up @@ -2248,6 +2251,30 @@ 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 "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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const FEATURES = Object.freeze({
orgUnitReplaceOuQueryParam: 'orgUnitReplaceOuQueryParam',
enrollmentStatusReplaceProgramStatusQueryParam: 'enrollmentStatusReplaceProgramStatusQueryParam',
emptyValueFilter: 'emptyValueFilter',
customTerminologyPlurals: 'customTerminologyPlurals',
});

const MINOR_VERSION_SUPPORT = Object.freeze({
Expand All @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 11 additions & 0 deletions src/core_modules/capture-core/HOC/withCustomLabels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';
import { useTermLabel, type TermRequest } from '../metaData';

export const withCustomLabels =
(requests: ReadonlyArray<TermRequest>) =>
(InnerComponent: React.ComponentType<any>) =>
(props: any) => {
const { programId, stageId } = props;
const labels = useTermLabel(requests, { programId, stageId });
return <InnerComponent {...props} {...labels} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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, LabelKeys } from '../../metaData';

type OwnProps = {
dataEntryId: string;
Expand All @@ -17,18 +18,26 @@ const makeMapStateToProps = () => {
const { dataEntries } = state;
const ready = !!dataEntries[dataEntryId];
const dataEntryKey = ready ? getDataEntryKey(dataEntryId, state.dataEntries[dataEntryId].itemId) : null;
// Example use of getTermLabel.
const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId: selectedScopeId });

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 },
),
};
};
};

export const DataEntryWidgetOutput: ComponentType<OwnProps> =
connect(makeMapStateToProps, () => ({}))(
(props: any) => (props.ready ? React.createElement(DataEntryWidgetOutputComponent, props) : null),
);
connect(makeMapStateToProps, () => ({}))(
(props: any) => (props.ready ? React.createElement(DataEntryWidgetOutputComponent, props) : null),
);
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ 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';
import { LoadingMaskElementCenter } from '../LoadingMasks';
import { Widget } from '../Widget';
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, getTermLabelFromProgram, LabelKeys } from '../../metaData';
import { convertValue } from '../../converters/clientToView';
import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName';
import { Date } from './Date';
Expand Down Expand Up @@ -94,13 +95,18 @@ const WidgetEnrollmentPlain = ({

const orgUnitClientValue = { id: enrollment?.orgUnit, name: orgUnitName, ancestors };
const ownerOrgUnitClientValue = { id: ownerOrgUnit?.id, name: ownerOrgUnitName, ancestors: ownerAncestors };
// Example use of getTermLabelFromProgram.
const { enrollmentLabel, followUpLabel } = getTermLabelFromProgram(
[LabelKeys.enrollmentSingular, LabelKeys.followUpSingular],
{ program },
);

return (
<div data-test="widget-enrollment">
<Widget
header={
<div className={classes.header}>
<span>{i18n.t('Enrollment')}</span>
<span>{capitalizeFirstLetter(enrollmentLabel)}</span>
{showWidgetBadge && (
<div className={classes.badge}>
<ReadOnlyBadge
Expand All @@ -126,7 +132,7 @@ const WidgetEnrollmentPlain = ({
<div className={classes.statuses} data-test="widget-enrollment-status">
{enrollment.followUp && (
<Tag negative>
{i18n.t('Follow-up')}
{capitalizeFirstLetter(followUpLabel)}
</Tag>
)}
<Status status={enrollment.status} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
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,' +
'displayNotesLabel,displayRelationshipsLabel,displayTrackedEntityAttributesLabel',
];

export const useProgram = (programId: string) => {
const { error, loading, data } = useDataQuery(
useMemo(
() => ({
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,
},
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { withStyles, WithStyles } from 'capture-core-utils/styles';
import { dataEntryIds } from 'capture-core/constants';
import type { ReduxAction } from 'capture-core-utils/types';
import i18n from '@dhis2/d2-i18n';
import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter';
import {
placements,
withCleanUp,
Expand Down Expand Up @@ -147,7 +148,8 @@ const buildOrgUnitSettingsFn = () => {
const orgUnitSettings = {
getComponent: () => viewModeComponent,
getComponentProps: (props: any) => createComponentProps(props, {
label: i18n.t('Organisation unit'),
// Example use of withCustomLabels.
label: capitalizeFirstLetter(props.orgUnitLabel),
valueConverter: value => dataElement.convertValue(value, valueConvertFn),
}),
getPropName: () => 'orgUnit',
Expand Down Expand Up @@ -223,7 +225,8 @@ const buildCompleteFieldSettingsFn = () => {
const completeSettings = {
getComponent: () => viewModeComponent,
getComponentProps: (props: any) => createComponentProps(props, {
label: i18n.t('Event completed'),
// Example use of withCustomLabels.
label: i18n.t('{{eventLabel}} completed', { eventLabel: props.eventLabel }),
id: dataElement.id,
valueConverter: value => dataElement.convertValue(value, valueConvertFn),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { connect } from 'react-redux';
import { ViewEventDataEntryComponent } from './ViewEventDataEntry.component';
import { withLoadingIndicator } from '../../../HOC/withLoadingIndicator';
import { withCustomLabels } from '../../../HOC/withCustomLabels';
import { LabelKeys } from '../../../metaData';

// Example use of withCustomLabels.
const customLabels = [LabelKeys.orgUnitSingular, LabelKeys.eventSingular] as const;

const mapStateToProps = (state: any, props: any) => {
const eventDetailsSection = state.viewEventPage.eventDetailsSection || {};
Expand All @@ -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)),
);
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ const WidgetEventEditPlain = ({
>
<ViewEventDataEntry
programId={programId}
stageId={stageId}
formFoundation={formFoundation}
dataEntryId={dataEntryIds.ENROLLMENT_EVENT}
hideDueDate={stage.hideDueDate}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Widget } from '../Widget';
import { ReadOnlyBadge } from '../ReadOnlyBadge';
import { Stages } from './Stages';
import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext';
import { useTermLabel, LabelKeys } from '../../metaData';
import type { Props } from './stagesAndEvents.types';

const styles = {
Expand Down Expand Up @@ -35,6 +36,11 @@ const WidgetStagesAndEventsPlain = ({
multipleStages,
showWidgetBadge,
} = useEnrollmentAccessContext();
// Example use of useTermLabel.
const { programStagesLabel, eventsLabel } = useTermLabel(
[LabelKeys.programStagePlural, LabelKeys.eventPlural],
{ programId },
);

return (
<div
Expand All @@ -44,7 +50,12 @@ const WidgetStagesAndEventsPlain = ({
<Widget
header={
<div className={classes.header}>
<span>{i18n.t('Stages and Events')}</span>
<span>
{i18n.t('{{programStagesLabel}} and {{eventsLabel}}', {
programStagesLabel,
eventsLabel,
})}
</span>
{showWidgetBadge && (
<div className={classes.badge}>
<ReadOnlyBadge
Expand Down
Loading
Loading