Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a139cb3
feat: support custom terminology labels for programs program stages a…
henrikmv Jun 13, 2026
52890a8
fix: clean up implementation
henrikmv Jun 13, 2026
cb457db
fix: remove lowercase option from label resolution functions
henrikmv Jun 13, 2026
d3da46e
feat: make resolver return the api custom string or undefined
henrikmv Jun 14, 2026
112cd38
feat: rename terms const
henrikmv Jun 14, 2026
506eda4
feat: custom terminology labels
henrikmv Jun 14, 2026
536969c
feat: update terminology types and labels to support custom labels
henrikmv Jun 15, 2026
1b899d0
fix: lint long line
henrikmv Jun 15, 2026
6729acf
Merge branch 'hv/feat/DHIS2-16610-support-custom-terminology' into hv…
henrikmv Jun 15, 2026
d8be21c
feat: update terminology and improve label handling in components
henrikmv Jun 17, 2026
9ab61f1
Merge branch 'master' into hv/feat/DHIS2-21635_configurable-terminolo…
henrikmv Jun 17, 2026
b78e268
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21635…
henrikmv Jun 25, 2026
a624f40
feat: revert useProgramLabel when only singular terminology available
henrikmv Jun 26, 2026
f490fe1
feat: refactor custom label fields to use singular and plural properties
henrikmv Jun 26, 2026
cb5903d
fix: (review) enhance custom label extraction with scope support for …
henrikmv Jun 26, 2026
cc289c4
feat: enhance terminology handling when counting
henrikmv Jun 27, 2026
78a5d85
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21635…
henrikmv Jun 27, 2026
c5cf892
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21635…
henrikmv Jul 3, 2026
1ea4cdc
Merge remote-tracking branch 'origin/master' into hv/feat/DHIS2-21635…
henrikmv Jul 3, 2026
d9d4ed5
Merge branch 'master' into hv/feat/DHIS2-21635_configurable-terminolo…
henrikmv Jul 20, 2026
a404ae0
fix: stop dispalying error onblur
henrikmv Jul 20, 2026
bbb0578
Revert "fix: stop dispalying error onblur"
henrikmv Jul 20, 2026
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
987 changes: 574 additions & 413 deletions i18n/en.pot

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { breadcrumbsKeys } from '../BulkDataEntryBreadcrumb';
import { useProgramLabel } from '../../../../metaData';

type Props = {
programId: string;
Expand All @@ -10,19 +11,16 @@ 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: { [key: string]: string },
) => {
if (selectedTemplate && !selectedTemplate.isDefault) {
return selectedTemplate.name;
}
if (selectedTemplateId && !selectedTemplate) {
return DefaultFilterLabels[selectedTemplateId as keyof typeof DefaultFilterLabels];
return defaultFilterLabels[selectedTemplateId];
}
return i18n.t('Program overview');
};
Expand All @@ -33,6 +31,14 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props)
const { selectedTemplateId, loading: isLoadingTemplates, templates } = workingListTemplates ?? {};
const selectedTemplate = templates?.find(({ id }: any) => id === selectedTemplateId);
const isSameProgram = workingListProgramId === programId;
const enrollments = useProgramLabel('enrollment', { plural: true, programId }) ?? i18n.t('enrollments');

const DefaultFilterLabels = useMemo(() => ({
default: i18n.t('Program overview'),
active: i18n.t('Active {{enrollments}}', { enrollments }),
complete: i18n.t('Completed {{enrollments}}', { enrollments }),
cancelled: i18n.t('Cancelled {{enrollments}}', { enrollments }),
}), [enrollments]);

const label = useMemo(() => {
if (page === breadcrumbsKeys.SEARCH_PAGE) {
Expand All @@ -44,7 +50,7 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props)
}

if (isSameProgram) {
return getWorkingListLabel(selectedTemplate, selectedTemplateId);
return getWorkingListLabel(selectedTemplate, selectedTemplateId, DefaultFilterLabels);
}

if (!displayFrontPageList) {
Expand All @@ -58,6 +64,7 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props)
selectedTemplate,
selectedTemplateId,
page,
DefaultFilterLabels,
]);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n';
import { withStyles, WithStyles } from 'capture-core-utils/styles';
import { colors } from '@dhis2/ui';
import { DirectionalChevron } from '../../../utils/rtl';
import { useProgramLabel, useStageLabel } from '../../../metaData';
import { useWorkingListLabel } from './hooks/useWorkingListLabel';
import { BreadcrumbItem } from '../common/BreadcrumbItem';
import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants';
Expand Down Expand Up @@ -74,6 +75,9 @@ const BreadcrumbsPlain = ({
displayFrontPageList,
});

const enrollment = useProgramLabel('enrollment', { programId }) ?? i18n.t('Enrollment');
const event = useStageLabel('event', { programId }) ?? i18n.t('event');

const handleNavigation = useCallback((callback?: () => void, warningType?: WarningKey) => {
if (userInteractionInProgress && warningType) {
setOpenWarning(warningType);
Expand Down Expand Up @@ -101,29 +105,29 @@ const BreadcrumbsPlain = ({
{
key: pageKeys.OVERVIEW,
onClick: () => handleNavigation(onBackToDashboard, pageKeys.OVERVIEW),
label: i18n.t('Enrollment dashboard'),
label: i18n.t('{{enrollment}} dashboard', { enrollment }),
selected: page === pageKeys.OVERVIEW,
condition: true,
},
{
key: pageKeys.VIEW_EVENT,
onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT),
label: i18n.t('View event'),
label: i18n.t('View {{event}}', { event }),
selected: page === pageKeys.VIEW_EVENT,
condition: page === pageKeys.VIEW_EVENT ||
(page === pageKeys.EDIT_EVENT && !eventIsScheduled(eventStatus)),
},
{
key: pageKeys.EDIT_EVENT,
onClick: () => undefined,
label: i18n.t('Edit event'),
label: i18n.t('Edit {{event}}', { event }),
selected: page === pageKeys.EDIT_EVENT,
condition: page === pageKeys.EDIT_EVENT,
},
{
key: pageKeys.NEW_EVENT,
onClick: () => undefined,
label: i18n.t('New event'),
label: i18n.t('New {{event}}', { event }),
selected: page === pageKeys.NEW_EVENT,
condition: page === pageKeys.NEW_EVENT,
},
Expand All @@ -135,6 +139,8 @@ const BreadcrumbsPlain = ({
onBackToMainPage,
onBackToDashboard,
onBackToViewEvent,
enrollment,
event,
]);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import i18n from '@dhis2/d2-i18n';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useProgramLabel } from '../../../../metaData';

type Template = {
id: string;
Expand All @@ -22,19 +23,20 @@ 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 workingListTemplates = useSelector((state: any) => state.workingListsTemplates?.teiList);
const workingListProgramId = useSelector((state: any) => state.workingListsContext?.teiList?.programIdView);
const enrollments = useProgramLabel('enrollment', { plural: true, programId }) ?? i18n.t('enrollments');

const DefaultFilterLabels: { [key in DefaultFilterKey]: string } = useMemo(() => ({
[DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'),
[DefaultFilterKeys.ACTIVE]: i18n.t('Active {{enrollments}}', { enrollments }),
[DefaultFilterKeys.COMPLETE]: i18n.t('Completed {{enrollments}}', { enrollments }),
[DefaultFilterKeys.CANCELLED]: i18n.t('Cancelled {{enrollments}}', { enrollments }),
}), [enrollments]);

const { selectedTemplateId, loading: isLoadingTemplates, templates } = workingListTemplates ?? {};

Expand Down Expand Up @@ -66,6 +68,7 @@ export const useWorkingListLabel = ({
isSameProgram,
selectedTemplate,
selectedTemplateId,
DefaultFilterLabels,
]);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n';
import { colors } from '@dhis2/ui';
import { withStyles, WithStyles } from 'capture-core-utils/styles';
import { DirectionalChevron } from '../../../utils/rtl';
import { useStageLabel } from '../../../metaData';
import { BreadcrumbItem } from '../common/BreadcrumbItem';
import { DiscardDialog } from '../../Dialogs/DiscardDialog.component';
import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants';
Expand Down Expand Up @@ -43,6 +44,7 @@ const EventBreadcrumbPlain = ({
}: Props) => {
const [openWarning, setOpenWarning] = useState<PageKeys | null>(null);
const { label } = useWorkingListLabel({ programId });
const event = useStageLabel('event', { programId }) ?? i18n.t('event');

const handleNavigation = useCallback((callback?: () => void, warningType?: PageKeys) => {
if (userInteractionInProgress && warningType) {
Expand Down Expand Up @@ -71,14 +73,14 @@ const EventBreadcrumbPlain = ({
{
key: pageKeys.VIEW_EVENT,
onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT),
label: i18n.t('View event'),
label: i18n.t('View {{event}}', { event }),
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: i18n.t('Edit {{event}}', { event }),
selected: page === pageKeys.EDIT_EVENT,
condition: page === pageKeys.EDIT_EVENT,
},
Expand All @@ -88,6 +90,7 @@ const EventBreadcrumbPlain = ({
onBackToViewEvent,
onBackToMainPage,
page,
event,
]);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import i18n from '@dhis2/d2-i18n';
import { useSelector } from 'react-redux';
import { useStageLabel } from '../../../../metaData';

type Template = {
id: string;
Expand All @@ -25,6 +26,7 @@ export const useWorkingListLabel = ({ programId }: Props) => {
const selectedTemplete: Template | undefined = templates?.find(({ id }) => id === selectedTemplateId);
const isDefaultTemplate: boolean | undefined = selectedTemplete?.isDefault;
const isSameProgram: boolean = workingListProgramId === programId;
const event = useStageLabel('event', { programId }) ?? i18n.t('Event');

const computedLabel: string = useMemo(() => {
if (loadingTemplates) return i18n.t('Loading...');
Expand All @@ -33,8 +35,8 @@ export const useWorkingListLabel = ({ programId }: Props) => {
return selectedTemplete.name;
}

return i18n.t('Event list');
}, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete]);
return i18n.t('{{event}} list', { event });
}, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete, event]);

return {
label: computedLabel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { availableCardListButtonState, enrollmentTypes } from './CardList.consta
import {
navigateToEnrollmentOverview,
} from '../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions';
import { useProgramLabel } from '../../metaData';

type Props = {
currentSearchScopeId?: string,
Expand Down Expand Up @@ -78,6 +79,8 @@ const CardListButtons: FC<Props> = ({
programName,
}) => {
const dispatch = useDispatch();
const programId = currentSearchScopeType === searchScopes.PROGRAM ? currentSearchScopeId : undefined;
const enrollment = useProgramLabel('enrollment', { programId }) ?? i18n.t('enrollment');
const navigationButtonsState: AvailableCardListButtonState = deriveNavigationButtonState(enrollmentType);
const onHandleClick: ButtonEventHandler<React.MouseEvent<HTMLButtonElement>> = useCallback((_, event) => {
event.stopPropagation();
Expand Down Expand Up @@ -115,7 +118,7 @@ const CardListButtons: FC<Props> = ({
{
dataTest: 'view-active-enrollment-button',
onClick: onHandleClick,
label: i18n.t('View active enrollment'),
label: i18n.t('View active {{enrollment}}', { enrollment }),
hide: navigationButtonsState !== availableCardListButtonState.SHOW_VIEW_ACTIVE_ENROLLMENT_BUTTON,
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
dataElementTypes,
getTrackerProgramThrowIfNotFound,
OptionSet,
useProgramLabel,
type TrackerProgram,
} from '../../metaData';
import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName';
Expand Down Expand Up @@ -156,6 +157,8 @@ const CardListItemIndex = ({
? deriveProgramFromEnrollment(enrollments, currentSearchScopeType)
: undefined;
const { fromServerDate } = useTimeZoneConversion();
const orgUnit = useProgramLabel('orgUnit', { programId: currentProgramId }) ?? i18n.t('Organisation unit');
const enrollment = useProgramLabel('enrollment', { programId: currentProgramId }) ?? i18n.t('enrollment');

const renderImageDataElement = (imageElement?: CardProfileImageElementInformation): React.ReactNode => {
if (!imageElement) { return null; }
Expand Down Expand Up @@ -217,11 +220,11 @@ const CardListItemIndex = ({

return (<>
<ListEntry
name={i18n.t('Organisation unit')}
name={orgUnit}
value={orgUnitName}
/>
<ListEntry
name={program?.enrollment?.enrollmentDateLabel ?? i18n.t('Date of enrollment')}
name={program?.enrollment?.enrollmentDateLabel ?? i18n.t('Date of {{enrollment}}', { enrollment })}
value={enrolledAt}
type={dataElementTypes.DATE}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { useProgramLabel } from '../../../../../metaData';

export const useDataEntrySections = (stageName: string, beforeSectionId: string) =>
useMemo(
export const useDataEntrySections = (stageName: string, beforeSectionId: string) => {
const enrollmentLabel = useProgramLabel('enrollment') ?? i18n.t('Enrollment');
return useMemo(
() => ({
[sectionKeysForFirstStageDataEntry.ENROLLMENT]: {
placement: placements.TOP,
name: i18n.t('Enrollment'),
name: enrollmentLabel,
},
[sectionKeysForFirstStageDataEntry.STAGE_BASIC_INFO]: {
beforeSectionId,
Expand All @@ -35,5 +37,6 @@ export const useDataEntrySections = (stageName: string, beforeSectionId: string)
}),
},
}),
[stageName, beforeSectionId],
[stageName, beforeSectionId, enrollmentLabel],
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { useStageLabel } from '../../../../../metaData';
import { listId } from './RecentlyAddedEventsList.const';
import type { Props } from './RecentlyAddedEventsList.types';

Expand All @@ -18,7 +19,10 @@ const styles = (theme: any) => ({

const NewEventsListPlain = (props: Props & WithStyles<typeof styles>) => {
const { classes, ...passOnProps } = props;
const eventSingular = useStageLabel('event') ?? i18n.t('event');
const eventPlural = useStageLabel('event', { plural: true }) ?? i18n.t('events');
const eventsAdded = props.events ? Object.keys(props.events).length : 0;
const noEventsText = i18n.t('No {{eventLabel}} added', { eventLabel: eventPlural });
if (eventsAdded === 0) {
return null;
}
Expand All @@ -27,16 +31,15 @@ const NewEventsListPlain = (props: Props & WithStyles<typeof styles>) => {
<div
className={classes.header}
>
{i18n.t('{{count}} event added', {
{i18n.t('{{count}} {{eventLabel}} added', {
count: eventsAdded,
defaultValue: '{{count}} event added',
defaultValue_plural: '{{count}} events added',
eventLabel: eventsAdded === 1 ? eventSingular : eventPlural,
})}
</div>
<OfflineEventsList
listId={listId}
noItemsText={i18n.t('No events added')}
emptyListText={i18n.t('No events added')}
noItemsText={noEventsText}
emptyListText={noEventsText}
{...passOnProps}
/>
</Card>
Expand Down
Loading
Loading