From a139cb3ccc7f79a5087ddd7f42835c8aa2413c72 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:29:34 +0200 Subject: [PATCH 01/15] feat: support custom terminology labels for programs program stages and tracked entity types --- i18n/en.pot | 82 ++++++++++- .../WidgetEnrollment/hooks/useProgram.ts | 3 + .../capture-core/metaData/Program/Program.ts | 13 ++ .../metaData/Program/ProgramStage.ts | 13 ++ .../TrackedEntityType/TrackedEntityType.ts | 13 ++ .../helpers/customLabels/applyCustomLabels.ts | 20 +++ .../helpers/customLabels/getCustomLabel.ts | 33 +++++ .../metaData/helpers/customLabels/index.ts | 11 ++ .../helpers/customLabels/labels.const.ts | 131 ++++++++++++++++++ .../helpers/customLabels/useCustomLabel.ts | 62 +++++++++ .../capture-core/metaData/helpers/index.ts | 15 ++ .../capture-core/metaData/index.ts | 13 ++ .../factory/program/ProgramFactory.ts | 3 + .../programStage/ProgramStageFactory.ts | 10 +- .../TrackedEntityTypeFactory.ts | 3 + .../quickStoreOperations/storePrograms.ts | 14 ++ .../types/apiPrograms.types.ts | 14 ++ .../storeTrackedEntityTypes.ts | 2 +- .../storageControllers/types/cache.types.ts | 15 ++ 19 files changed, 466 insertions(+), 4 deletions(-) create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/index.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts diff --git a/i18n/en.pot b/i18n/en.pot index b89585a062..4c8c8f42db 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-06-10T13:04:43.537Z\n" -"PO-Revision-Date: 2026-06-10T13:04:43.537Z\n" +"POT-Creation-Date: 2026-06-13T18:27:09.137Z\n" +"PO-Revision-Date: 2026-06-13T18:27:09.138Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2210,6 +2210,84 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" +msgid "Enrollments" +msgstr "Enrollments" + +msgid "Relationship" +msgstr "Relationship" + +msgid "Note" +msgstr "Note" + +msgid "Attribute" +msgstr "Attribute" + +msgid "Program stage" +msgstr "Program stage" + +msgid "Program stages" +msgstr "Program stages" + +msgid "Event" +msgstr "Event" + +msgid "Events" +msgstr "Events" + +msgid "Tracked entity types" +msgstr "Tracked entity types" + +msgid "enrollments" +msgstr "enrollments" + +msgid "follow-up" +msgstr "follow-up" + +msgid "organisation unit" +msgstr "organisation unit" + +msgid "relationship" +msgstr "relationship" + +msgid "note" +msgstr "note" + +msgid "attribute" +msgstr "attribute" + +msgid "program stage" +msgstr "program stage" + +msgid "program stages" +msgstr "program stages" + +msgid "tracked entity types" +msgstr "tracked entity types" + +msgid "Follow-ups" +msgstr "Follow-ups" + +msgid "Organisation units" +msgstr "Organisation units" + +msgid "Attributes" +msgstr "Attributes" + +msgid "follow-ups" +msgstr "follow-ups" + +msgid "organisation units" +msgstr "organisation units" + +msgid "relationships" +msgstr "relationships" + +msgid "notes" +msgstr "notes" + +msgid "attributes" +msgstr "attributes" + msgid "Program not found" msgstr "Program not found" 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 6b59c7005e..048ac5018c 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -15,6 +15,9 @@ export const useProgram = (programId: string) => { 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', diff --git a/src/core_modules/capture-core/metaData/Program/Program.ts b/src/core_modules/capture-core/metaData/Program/Program.ts index 6f5f948961..7103209e9b 100644 --- a/src/core_modules/capture-core/metaData/Program/Program.ts +++ b/src/core_modules/capture-core/metaData/Program/Program.ts @@ -6,6 +6,7 @@ import type{ CategoryCombination } from '../CategoryCombinations'; import type { Icon } from '../Icon'; import type { Access } from '../Access'; import type { ProgramStage } from './ProgramStage'; +import type { CustomLabels, LabelKey } from '../helpers/customLabels'; export class Program { static errorMessages = { @@ -26,6 +27,7 @@ export class Program { _displayFrontPageList!: boolean; _useFirstStageDuringRegistration!: boolean; _onlyEnrollOnce!: boolean; + _customLabels!: CustomLabels; constructor(initFn?: (_this: Program) => void) { this.programRules = []; @@ -33,6 +35,7 @@ export class Program { this.organisationUnits = {}; this._stages = new Map(); this._organisationUnits = {}; + this._customLabels = {}; initFn && isFunction(initFn) && initFn(this); } @@ -129,6 +132,16 @@ export class Program { return this._onlyEnrollOnce; } + get customLabels(): CustomLabels { + return this._customLabels; + } + + setCustomLabel(key: LabelKey, value?: string | null) { + if (value) { + this._customLabels[key] = value; + } + } + get stages(): Map { return this._stages; } diff --git a/src/core_modules/capture-core/metaData/Program/ProgramStage.ts b/src/core_modules/capture-core/metaData/Program/ProgramStage.ts index dcd71cd84a..46d4d7ac6b 100644 --- a/src/core_modules/capture-core/metaData/Program/ProgramStage.ts +++ b/src/core_modules/capture-core/metaData/Program/ProgramStage.ts @@ -6,6 +6,7 @@ import type { ProgramRule } from '@dhis2/rules-engine-javascript'; import type { Icon } from '../Icon'; import type { RenderFoundation } from '../RenderFoundation'; import type { RelationshipType } from '../RelationshipType'; +import type { CustomLabels, LabelKey } from '../helpers/customLabels'; export class ProgramStage { _id!: string; @@ -27,9 +28,11 @@ export class ProgramStage { _minDaysFromStart!: number; _icon!: Icon | undefined; _programRules!: Array; + _customLabels!: CustomLabels; constructor(initFn: ((_this: ProgramStage) => void) | null) { this.programRules = []; + this._customLabels = {}; initFn && isFunction(initFn) && initFn(this); } @@ -192,4 +195,14 @@ export class ProgramStage { get repeatable(): boolean { return this._repeatable; } + + get customLabels(): CustomLabels { + return this._customLabels; + } + + setCustomLabel(key: LabelKey, value?: string | null) { + if (value) { + this._customLabels[key] = value; + } + } } diff --git a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts index 0e48280536..99374e64e1 100644 --- a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts +++ b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts @@ -3,6 +3,7 @@ import type { SearchGroup } from '../SearchGroup'; import type { DataElement } from '../DataElement'; import type { TeiRegistration } from './TeiRegistration'; import type { Access } from '../Access'; +import type { CustomLabels, LabelKey } from '../helpers/customLabels'; /* eslint-disable no-underscore-dangle */ export class TrackedEntityType { @@ -12,9 +13,11 @@ 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); } @@ -59,4 +62,14 @@ export class TrackedEntityType { get attributes(): Array { return this._attributes; } + + get customLabels(): CustomLabels { + return this._customLabels; + } + + setCustomLabel(key: LabelKey, value?: string | null) { + if (value) { + this._customLabels[key] = value; + } + } } diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts new file mode 100644 index 0000000000..60fbeeab3f --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts @@ -0,0 +1,20 @@ +import type { LabelKey } from './labels.const'; + +type LabelTarget = { setCustomLabel: (key: LabelKey, value?: string | null) => void }; + +/** + * Copies the configured custom labels from a cached metadata object onto the + * in-memory metadata instance, using the key → cached-field mapping. Only + * present values are stored (setCustomLabel ignores empty values). + */ +export const applyCustomLabels = ( + target: LabelTarget, + cached: { [key: string]: any }, + fieldMap: { [key in LabelKey]?: string }, +) => { + Object.entries(fieldMap).forEach(([key, fieldName]) => { + if (fieldName) { + target.setCustomLabel(key as LabelKey, cached[fieldName]); + } + }); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts new file mode 100644 index 0000000000..9a0ef4141d --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts @@ -0,0 +1,33 @@ +import { getDefaultLabel } from './labels.const'; +import type { LabelKey } from './labels.const'; +import type { Program } from '../../Program/Program'; +import type { ProgramStage } from '../../Program/ProgramStage'; +import type { TrackedEntityType } from '../../TrackedEntityType/TrackedEntityType'; + +type Options = { lowercase?: boolean, plural?: boolean }; + +/** + * Resolves the configurable terminology label for a program-level term, falling + * back to the translated default when no custom label is configured. + */ +export const getProgramLabel = (program: Program | undefined | null, key: LabelKey, options?: Options): string => + program?.customLabels[key] ?? getDefaultLabel(key, options); + +/** + * Resolves a term that can be overridden at both stage and program level. + * Precedence: stage label → program label → translated default. + */ +export const getStageLabel = ( + stage: ProgramStage | undefined | null, + program: Program | undefined | null, + key: LabelKey, + options?: Options, +): string => + stage?.customLabels[key] ?? program?.customLabels[key] ?? getDefaultLabel(key, options); + +export const getTrackedEntityTypeLabel = ( + trackedEntityType: TrackedEntityType | undefined | null, + key: LabelKey, + options?: Options, +): string => + trackedEntityType?.customLabels[key] ?? getDefaultLabel(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 new file mode 100644 index 0000000000..4f88fc8773 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -0,0 +1,11 @@ +export { + labelKeys, + getDefaultLabel, + programLabelFields, + programStageLabelFields, + trackedEntityTypeLabelFields, +} from './labels.const'; +export type { LabelKey, CustomLabels } from './labels.const'; +export { getProgramLabel, getStageLabel, getTrackedEntityTypeLabel } from './getCustomLabel'; +export { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } from './useCustomLabel'; +export { applyCustomLabels } from './applyCustomLabels'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts new file mode 100644 index 0000000000..2a9ca0b7c4 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts @@ -0,0 +1,131 @@ +import i18n from '@dhis2/d2-i18n'; + +/** + * Keys for the configurable terminology labels (DHIS2-16581). + * A program / program stage / tracked entity type can override the default term + * shown in the UI; when no custom label is configured we fall back to the + * translated default (see getDefaultLabel). + */ +// Keys map 1:1 to the configurable label fields exposed by the backend / MMA. +// Singular-only terms (no backend plural) are pluralised at render time via the +// `plural` option on the resolver, which reuses the singular custom value. +export const labelKeys = Object.freeze({ + ENROLLMENT: 'ENROLLMENT', + ENROLLMENTS: 'ENROLLMENTS', + FOLLOW_UP: 'FOLLOW_UP', + ORG_UNIT: 'ORG_UNIT', + RELATIONSHIP: 'RELATIONSHIP', + NOTE: 'NOTE', + TRACKED_ENTITY_ATTRIBUTE: 'TRACKED_ENTITY_ATTRIBUTE', + PROGRAM_STAGE: 'PROGRAM_STAGE', + PROGRAM_STAGES: 'PROGRAM_STAGES', + EVENT: 'EVENT', + EVENTS: 'EVENTS', + TRACKED_ENTITY_TYPES: 'TRACKED_ENTITY_TYPES', +}); + +export type LabelKey = typeof labelKeys[keyof typeof labelKeys]; + +export type CustomLabels = { [key in LabelKey]?: string }; + +/** + * Maps each label key to the cached metadata field that holds the custom value. + * Used by the metadata factories to populate the sparse customLabels map. + */ +export const programLabelFields: { [key in LabelKey]?: string } = Object.freeze({ + [labelKeys.ENROLLMENT]: 'displayEnrollmentLabel', + [labelKeys.ENROLLMENTS]: 'displayEnrollmentsLabel', + [labelKeys.FOLLOW_UP]: 'displayFollowUpLabel', + [labelKeys.ORG_UNIT]: 'displayOrgUnitLabel', + [labelKeys.RELATIONSHIP]: 'displayRelationshipLabel', + [labelKeys.NOTE]: 'displayNoteLabel', + [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: 'displayTrackedEntityAttributeLabel', + [labelKeys.PROGRAM_STAGE]: 'displayProgramStageLabel', + [labelKeys.PROGRAM_STAGES]: 'displayProgramStagesLabel', + [labelKeys.EVENT]: 'displayEventLabel', + [labelKeys.EVENTS]: 'displayEventsLabel', +}); + +export const programStageLabelFields: { [key in LabelKey]?: string } = Object.freeze({ + [labelKeys.PROGRAM_STAGE]: 'displayProgramStageLabel', + [labelKeys.EVENT]: 'displayEventLabel', + [labelKeys.EVENTS]: 'displayEventsLabel', +}); + +export const trackedEntityTypeLabelFields: { [key in LabelKey]?: string } = Object.freeze({ + [labelKeys.TRACKED_ENTITY_TYPES]: 'displayTrackedEntityTypesLabel', +}); + +// Built lazily (inside getDefaultLabel) so i18n.t() resolves against the active locale. +const getCapitalizedDefaults = (): { [key in LabelKey]: string } => ({ + [labelKeys.ENROLLMENT]: i18n.t('Enrollment'), + [labelKeys.ENROLLMENTS]: i18n.t('Enrollments'), + [labelKeys.FOLLOW_UP]: i18n.t('Follow-up'), + [labelKeys.ORG_UNIT]: i18n.t('Organisation unit'), + [labelKeys.RELATIONSHIP]: i18n.t('Relationship'), + [labelKeys.NOTE]: i18n.t('Note'), + [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('Attribute'), + [labelKeys.PROGRAM_STAGE]: i18n.t('Program stage'), + [labelKeys.PROGRAM_STAGES]: i18n.t('Program stages'), + [labelKeys.EVENT]: i18n.t('Event'), + [labelKeys.EVENTS]: i18n.t('Events'), + [labelKeys.TRACKED_ENTITY_TYPES]: i18n.t('Tracked entity types'), +}); + +// Mid-sentence form (e.g. "Are you sure you want to delete this event?"). +const getLowercaseDefaults = (): { [key in LabelKey]: string } => ({ + [labelKeys.ENROLLMENT]: i18n.t('enrollment'), + [labelKeys.ENROLLMENTS]: i18n.t('enrollments'), + [labelKeys.FOLLOW_UP]: i18n.t('follow-up'), + [labelKeys.ORG_UNIT]: i18n.t('organisation unit'), + [labelKeys.RELATIONSHIP]: i18n.t('relationship'), + [labelKeys.NOTE]: i18n.t('note'), + [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('attribute'), + [labelKeys.PROGRAM_STAGE]: i18n.t('program stage'), + [labelKeys.PROGRAM_STAGES]: i18n.t('program stages'), + [labelKeys.EVENT]: i18n.t('event'), + [labelKeys.EVENTS]: i18n.t('events'), + [labelKeys.TRACKED_ENTITY_TYPES]: i18n.t('tracked entity types'), +}); + +// Plural defaults for singular-only terms (no backend plural field). Used when a +// plural UI slot has no custom label configured; if a custom label IS configured +// the singular custom value is used instead (see resolver). +const getCapitalizedPluralDefaults = (): { [key in LabelKey]?: string } => ({ + [labelKeys.FOLLOW_UP]: i18n.t('Follow-ups'), + [labelKeys.ORG_UNIT]: i18n.t('Organisation units'), + [labelKeys.RELATIONSHIP]: i18n.t('Relationships'), + [labelKeys.NOTE]: i18n.t('Notes'), + [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('Attributes'), +}); + +const getLowercasePluralDefaults = (): { [key in LabelKey]?: string } => ({ + [labelKeys.FOLLOW_UP]: i18n.t('follow-ups'), + [labelKeys.ORG_UNIT]: i18n.t('organisation units'), + [labelKeys.RELATIONSHIP]: i18n.t('relationships'), + [labelKeys.NOTE]: i18n.t('notes'), + [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('attributes'), +}); + +type GetDefaultLabelOptions = { lowercase?: boolean, plural?: boolean }; + +/** + * Returns the translated default term for a label key. Custom labels (entered by + * the implementer) are used verbatim; only this default is translated/cased here. + * `plural` provides a plural default for singular-only terms (note, relationship, + * org unit, attribute, follow-up) which have no dedicated plural field. + */ +export const getDefaultLabel = ( + key: LabelKey, + { lowercase = false, plural = false }: GetDefaultLabelOptions = {}, +): string => { + if (plural) { + const pluralDefaults = lowercase ? getLowercasePluralDefaults() : getCapitalizedPluralDefaults(); + const pluralDefault = pluralDefaults[key]; + if (pluralDefault) { + return pluralDefault; + } + } + const defaults = lowercase ? getLowercaseDefaults() : getCapitalizedDefaults(); + return defaults[key]; +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts new file mode 100644 index 0000000000..ea63d3d8e9 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts @@ -0,0 +1,62 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; +import { getProgramLabel, getStageLabel, getTrackedEntityTypeLabel } from './getCustomLabel'; +import type { LabelKey } from './labels.const'; + +type Options = { lowercase?: boolean, plural?: boolean }; + +/** + * Resolves a program-level configurable terminology label. Defaults to the + * currently selected program when no programId is passed. + */ +export const useProgramLabel = (key: LabelKey, programIdInput?: string, options?: Options): string => { + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const programId = programIdInput ?? currentProgramId; + const lowercase = options?.lowercase; + const plural = options?.plural; + return useMemo( + () => getProgramLabel(programId ? programCollection.get(programId) : undefined, key, { lowercase, plural }), + [programId, key, lowercase, plural], + ); +}; + +/** + * Resolves a term that can be overridden at both stage and program level + * (stage label → program label → default). Defaults to the currently selected + * program/stage when ids are not passed. + */ +export const useStageLabel = ( + key: LabelKey, + stageIdInput?: string, + programIdInput?: string, + options?: Options, +): string => { + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const currentStageId = useSelector(({ currentSelections }: any) => currentSelections.stageId); + const programId = programIdInput ?? currentProgramId; + const stageId = stageIdInput ?? currentStageId; + const lowercase = options?.lowercase; + const plural = options?.plural; + return useMemo(() => { + const program = programId ? programCollection.get(programId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + return getStageLabel(stage, program, key, { lowercase, plural }); + }, [programId, stageId, key, lowercase, plural]); +}; + +/** + * Resolves a tracked-entity-type-level configurable terminology label. Defaults + * to the currently selected tracked entity type when no id is passed. + */ +export const useTrackedEntityTypeLabel = (key: LabelKey, tetIdInput?: string, options?: Options): string => { + const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); + const tetId = tetIdInput ?? currentTetId; + const lowercase = options?.lowercase; + const plural = options?.plural; + return useMemo( + () => getTrackedEntityTypeLabel( + tetId ? trackedEntityTypesCollection.get(tetId) : undefined, key, { lowercase, plural }), + [tetId, key, lowercase, plural], + ); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index bafd05df30..22b1de92bc 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -17,3 +17,18 @@ export { getScopeInfo } from './getScopeInfo'; export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; +export { + labelKeys, + getDefaultLabel, + getProgramLabel, + getStageLabel, + getTrackedEntityTypeLabel, + useProgramLabel, + useStageLabel, + useTrackedEntityTypeLabel, + applyCustomLabels, + programLabelFields, + programStageLabelFields, + trackedEntityTypeLabelFields, +} from './customLabels'; +export type { LabelKey, CustomLabels } from './customLabels'; diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 08ca630163..a97b364750 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -40,4 +40,17 @@ export { getProgramThrowIfNotFound, getProgramAndStageForEventProgram, getEventProgramEventAccess, + labelKeys, + getDefaultLabel, + getProgramLabel, + getStageLabel, + getTrackedEntityTypeLabel, + useProgramLabel, + useStageLabel, + useTrackedEntityTypeLabel, + applyCustomLabels, + programLabelFields, + programStageLabelFields, + trackedEntityTypeLabelFields, } from './helpers'; +export type { LabelKey, CustomLabels } from './helpers'; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts index 5eedd0e034..07ff10a249 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts @@ -5,6 +5,8 @@ import { EventProgram, TrackerProgram, CategoryCombination, + applyCustomLabels, + programLabelFields, type TrackedEntityType, type Category, } from '../../../../metaData'; @@ -187,6 +189,7 @@ export class ProgramFactory { program.displayFrontPageList = cachedProgram.displayFrontPageList; program.onlyEnrollOnce = cachedProgram.onlyEnrollOnce; program.useFirstStageDuringRegistration = cachedProgram.useFirstStageDuringRegistration; + applyCustomLabels(program, cachedProgram, programLabelFields); return program; } 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 8b8ed4b130..fbcf1a5e69 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 @@ -12,7 +12,14 @@ import type { CachedOptionSet, CachedDataElement, } from '../../../../storageControllers'; -import { Section, ProgramStage, RenderFoundation, CustomForm } from '../../../../metaData'; +import { + Section, + ProgramStage, + RenderFoundation, + CustomForm, + applyCustomLabels, + programStageLabelFields, +} from '../../../../metaData'; import { buildIcon } from '../../../common/helpers'; import { isNonEmptyArray } from '../../../../utils/isNonEmptyArray'; import { DataElementFactory } from './DataElementFactory'; @@ -242,6 +249,7 @@ export class ProgramStageFactory { camelCaseUppercaseString(cachedProgramStage.validationStrategy); }); _stage.icon = buildIcon(cachedProgramStage.style); + applyCustomLabels(_stage, cachedProgramStage, programStageLabelFields); }); const stageForm = stage.stageForm; 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 2f61aa57f0..cbe29f1b09 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,6 +1,8 @@ /* eslint-disable no-underscore-dangle */ import { TrackedEntityType, + applyCustomLabels, + trackedEntityTypeLabelFields, } from '../../../../metaData'; import { DataElementFactory } from './DataElementFactory'; import { TeiRegistrationFactory } from './TeiRegistrationFactory'; @@ -86,6 +88,7 @@ export class TrackedEntityTypeFactory { o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; + applyCustomLabels(o, cachedType, trackedEntityTypeLabelFields); }); 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 f36ad26c90..d6c11ed97c 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -115,6 +115,9 @@ const programStageFields = [ 'description', 'displayExecutionDateLabel', 'displayDueDateLabel', + 'displayProgramStageLabel', + 'displayEventLabel', + 'displayEventsLabel', 'formType', 'featureType', 'validationStrategy', @@ -134,6 +137,17 @@ const fieldsParam = [ 'displayFrontPageList', 'displayIncidentDateLabel', 'displayEnrollmentDateLabel', + 'displayEnrollmentLabel', + 'displayEnrollmentsLabel', + 'displayFollowUpLabel', + 'displayOrgUnitLabel', + 'displayRelationshipLabel', + 'displayNoteLabel', + 'displayTrackedEntityAttributeLabel', + 'displayProgramStageLabel', + 'displayProgramStagesLabel', + 'displayEventLabel', + 'displayEventsLabel', 'minAttributesRequiredToSearch', 'useFirstStageDuringRegistration', 'onlyEnrollOnce', 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 091ffbfe9c..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 @@ -102,6 +102,9 @@ export type apiProgramStage = { description?: string | null, displayExecutionDateLabel?: string | null, displayDueDateLabel?: string | null, + displayProgramStageLabel?: string | null, + displayEventLabel?: string | null, + displayEventsLabel?: string | null, formType: string, featureType: string, validationStrategy: string, @@ -139,6 +142,17 @@ type apiProgram = { minAttributesRequiredToSearch: number, displayEnrollmentDateLabel?: string | null, displayIncidentDateLabel?: string | null, + displayEnrollmentLabel?: string | null, + displayEnrollmentsLabel?: string | null, + displayFollowUpLabel?: string | null, + displayOrgUnitLabel?: string | null, + displayRelationshipLabel?: string | null, + displayNoteLabel?: string | null, + displayTrackedEntityAttributeLabel?: string | null, + displayProgramStageLabel?: string | null, + displayProgramStagesLabel?: string | null, + displayEventLabel?: string | null, + displayEventsLabel?: string | null, featureType?: string | null, selectEnrollmentDatesInFuture: boolean, displayFrontPageList: boolean, 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 a193f3d78f..1e6f04c161 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,7 @@ const convert = (() => { })); })(); -const fieldsParam = 'id,access,displayName,minAttributesRequiredToSearch,featureType,' + +const fieldsParam = 'id,access,displayName,displayTrackedEntityTypesLabel,minAttributesRequiredToSearch,featureType,' + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + 'translations[property,locale,value]'; 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 108e2f4697..771a55c14b 100644 --- a/src/core_modules/capture-core/storageControllers/types/cache.types.ts +++ b/src/core_modules/capture-core/storageControllers/types/cache.types.ts @@ -109,6 +109,9 @@ export type CachedProgramStage = { description: string | null, displayExecutionDateLabel?: string | null, displayDueDateLabel?: string | null, + displayProgramStageLabel?: string | null, + displayEventLabel?: string | null, + displayEventsLabel?: string | null, programStageSections?: Array | null, programStageDataElements: Array, formType: string, @@ -181,6 +184,7 @@ export type CachedTrackedEntityType = { id: string, access: Access, displayName: string, + displayTrackedEntityTypesLabel?: string | null, trackedEntityTypeAttributes?: Array | null, translations: Array, minAttributesRequiredToSearch: number, @@ -206,6 +210,17 @@ export type CachedProgram = { enrollmentDateLabel?: string | null, displayEnrollmentDateLabel: string, displayIncidentDateLabel: string, + displayEnrollmentLabel?: string | null, + displayEnrollmentsLabel?: string | null, + displayFollowUpLabel?: string | null, + displayOrgUnitLabel?: string | null, + displayRelationshipLabel?: string | null, + displayNoteLabel?: string | null, + displayTrackedEntityAttributeLabel?: string | null, + displayProgramStageLabel?: string | null, + displayProgramStagesLabel?: string | null, + displayEventLabel?: string | null, + displayEventsLabel?: string | null, dataEntryForm?: CachedDataEntryForm | null, featureType: 'POINT' | 'POLYGON' | null, selectEnrollmentDatesInFuture: boolean, From 52890a867736e755698180922e4ba27e6da370b8 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 13 Jun 2026 21:35:52 +0200 Subject: [PATCH 02/15] fix: clean up implementation --- i18n/en.pot | 100 +++++------ .../capture-core/metaData/Program/Program.ts | 11 +- .../metaData/Program/ProgramStage.ts | 11 +- .../TrackedEntityType/TrackedEntityType.ts | 11 +- .../helpers/customLabels/applyCustomLabels.ts | 20 --- .../helpers/customLabels/customLabels.ts | 157 ++++++++++++++++++ .../helpers/customLabels/getCustomLabel.ts | 33 ---- .../metaData/helpers/customLabels/index.ts | 19 +-- .../helpers/customLabels/labels.const.ts | 131 --------------- .../helpers/customLabels/useCustomLabel.ts | 62 ------- .../metaData/helpers/customLabels/useLabel.ts | 45 +++++ .../capture-core/metaData/helpers/index.ts | 11 +- .../capture-core/metaData/index.ts | 11 +- .../factory/program/ProgramFactory.ts | 5 +- .../programStage/ProgramStageFactory.ts | 5 +- .../TrackedEntityTypeFactory.ts | 5 +- 16 files changed, 290 insertions(+), 347 deletions(-) delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts delete mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts create mode 100644 src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts diff --git a/i18n/en.pot b/i18n/en.pot index 4c8c8f42db..f288bc6231 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-06-13T18:27:09.137Z\n" -"PO-Revision-Date: 2026-06-13T18:27:09.138Z\n" +"POT-Creation-Date: 2026-06-13T19:35:53.319Z\n" +"PO-Revision-Date: 2026-06-13T19:35:53.320Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2213,80 +2213,86 @@ msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" msgid "Enrollments" msgstr "Enrollments" -msgid "Relationship" -msgstr "Relationship" - -msgid "Note" -msgstr "Note" - -msgid "Attribute" -msgstr "Attribute" - -msgid "Program stage" -msgstr "Program stage" - -msgid "Program stages" -msgstr "Program stages" - -msgid "Event" -msgstr "Event" - -msgid "Events" -msgstr "Events" - -msgid "Tracked entity types" -msgstr "Tracked entity types" - msgid "enrollments" msgstr "enrollments" +msgid "Follow-ups" +msgstr "Follow-ups" + msgid "follow-up" msgstr "follow-up" +msgid "follow-ups" +msgstr "follow-ups" + +msgid "Organisation units" +msgstr "Organisation units" + msgid "organisation unit" msgstr "organisation unit" +msgid "organisation units" +msgstr "organisation units" + +msgid "Relationship" +msgstr "Relationship" + msgid "relationship" msgstr "relationship" +msgid "relationships" +msgstr "relationships" + +msgid "Note" +msgstr "Note" + msgid "note" msgstr "note" +msgid "notes" +msgstr "notes" + +msgid "Attribute" +msgstr "Attribute" + +msgid "Attributes" +msgstr "Attributes" + msgid "attribute" msgstr "attribute" +msgid "attributes" +msgstr "attributes" + +msgid "Program stage" +msgstr "Program stage" + +msgid "Program stages" +msgstr "Program stages" + msgid "program stage" msgstr "program stage" msgid "program stages" msgstr "program stages" -msgid "tracked entity types" -msgstr "tracked entity types" - -msgid "Follow-ups" -msgstr "Follow-ups" - -msgid "Organisation units" -msgstr "Organisation units" +msgid "Event" +msgstr "Event" -msgid "Attributes" -msgstr "Attributes" +msgid "Events" +msgstr "Events" -msgid "follow-ups" -msgstr "follow-ups" +msgid "Tracked entity type" +msgstr "Tracked entity type" -msgid "organisation units" -msgstr "organisation units" +msgid "Tracked entity types" +msgstr "Tracked entity types" -msgid "relationships" -msgstr "relationships" +msgid "tracked entity type" +msgstr "tracked entity type" -msgid "notes" -msgstr "notes" - -msgid "attributes" -msgstr "attributes" +msgid "tracked entity types" +msgstr "tracked entity types" msgid "Program not found" msgstr "Program not found" diff --git a/src/core_modules/capture-core/metaData/Program/Program.ts b/src/core_modules/capture-core/metaData/Program/Program.ts index 7103209e9b..b0ad439274 100644 --- a/src/core_modules/capture-core/metaData/Program/Program.ts +++ b/src/core_modules/capture-core/metaData/Program/Program.ts @@ -6,7 +6,7 @@ import type{ CategoryCombination } from '../CategoryCombinations'; import type { Icon } from '../Icon'; import type { Access } from '../Access'; import type { ProgramStage } from './ProgramStage'; -import type { CustomLabels, LabelKey } from '../helpers/customLabels'; +import type { CustomLabels } from '../helpers/customLabels'; export class Program { static errorMessages = { @@ -132,16 +132,13 @@ export class Program { return this._onlyEnrollOnce; } + set customLabels(customLabels: CustomLabels) { + this._customLabels = customLabels; + } get customLabels(): CustomLabels { return this._customLabels; } - setCustomLabel(key: LabelKey, value?: string | null) { - if (value) { - this._customLabels[key] = value; - } - } - get stages(): Map { return this._stages; } diff --git a/src/core_modules/capture-core/metaData/Program/ProgramStage.ts b/src/core_modules/capture-core/metaData/Program/ProgramStage.ts index 46d4d7ac6b..caebc666ff 100644 --- a/src/core_modules/capture-core/metaData/Program/ProgramStage.ts +++ b/src/core_modules/capture-core/metaData/Program/ProgramStage.ts @@ -6,7 +6,7 @@ import type { ProgramRule } from '@dhis2/rules-engine-javascript'; import type { Icon } from '../Icon'; import type { RenderFoundation } from '../RenderFoundation'; import type { RelationshipType } from '../RelationshipType'; -import type { CustomLabels, LabelKey } from '../helpers/customLabels'; +import type { CustomLabels } from '../helpers/customLabels'; export class ProgramStage { _id!: string; @@ -196,13 +196,10 @@ export class ProgramStage { return this._repeatable; } + set customLabels(customLabels: CustomLabels) { + this._customLabels = customLabels; + } get customLabels(): CustomLabels { return this._customLabels; } - - setCustomLabel(key: LabelKey, value?: string | null) { - if (value) { - this._customLabels[key] = value; - } - } } diff --git a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts index 99374e64e1..e3ba50563e 100644 --- a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts +++ b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts @@ -3,7 +3,7 @@ import type { SearchGroup } from '../SearchGroup'; import type { DataElement } from '../DataElement'; import type { TeiRegistration } from './TeiRegistration'; import type { Access } from '../Access'; -import type { CustomLabels, LabelKey } from '../helpers/customLabels'; +import type { CustomLabels } from '../helpers/customLabels'; /* eslint-disable no-underscore-dangle */ export class TrackedEntityType { @@ -63,13 +63,10 @@ export class TrackedEntityType { return this._attributes; } + set customLabels(customLabels: CustomLabels) { + this._customLabels = customLabels; + } get customLabels(): CustomLabels { return this._customLabels; } - - setCustomLabel(key: LabelKey, value?: string | null) { - if (value) { - this._customLabels[key] = value; - } - } } diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts deleted file mode 100644 index 60fbeeab3f..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/applyCustomLabels.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { LabelKey } from './labels.const'; - -type LabelTarget = { setCustomLabel: (key: LabelKey, value?: string | null) => void }; - -/** - * Copies the configured custom labels from a cached metadata object onto the - * in-memory metadata instance, using the key → cached-field mapping. Only - * present values are stored (setCustomLabel ignores empty values). - */ -export const applyCustomLabels = ( - target: LabelTarget, - cached: { [key: string]: any }, - fieldMap: { [key in LabelKey]?: string }, -) => { - Object.entries(fieldMap).forEach(([key, fieldName]) => { - if (fieldName) { - target.setCustomLabel(key as LabelKey, cached[fieldName]); - } - }); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts new file mode 100644 index 0000000000..38de0dfce0 --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -0,0 +1,157 @@ +import i18n from '@dhis2/d2-i18n'; + +/** + * Configurable terminology (DHIS2-16581 + v43 plurals). + * + * Single source of truth: each term declares the backend field(s) that hold its + * custom value and the translated defaults. The resolver returns the custom label + * if configured, otherwise the translated default. + * + * Default strings are wrapped in thunks (not evaluated at module load) so i18n.t() + * resolves against the active locale, and are written as literals so the i18n + * extractor can find them. + */ +type TermDef = { + field?: string, + pluralField?: string, + singular: () => string, + plural: () => string, + singularLower: () => string, + pluralLower: () => string, +}; + +export const TERMS: { [key: string]: TermDef } = { + enrollment: { + field: 'displayEnrollmentLabel', + pluralField: 'displayEnrollmentsLabel', + singular: () => i18n.t('Enrollment'), + plural: () => i18n.t('Enrollments'), + singularLower: () => i18n.t('enrollment'), + pluralLower: () => i18n.t('enrollments'), + }, + followUp: { + field: 'displayFollowUpLabel', + singular: () => i18n.t('Follow-up'), + plural: () => i18n.t('Follow-ups'), + singularLower: () => i18n.t('follow-up'), + pluralLower: () => i18n.t('follow-ups'), + }, + orgUnit: { + field: 'displayOrgUnitLabel', + singular: () => i18n.t('Organisation unit'), + plural: () => i18n.t('Organisation units'), + singularLower: () => i18n.t('organisation unit'), + pluralLower: () => i18n.t('organisation units'), + }, + relationship: { + field: 'displayRelationshipLabel', + singular: () => i18n.t('Relationship'), + plural: () => i18n.t('Relationships'), + singularLower: () => i18n.t('relationship'), + pluralLower: () => i18n.t('relationships'), + }, + note: { + field: 'displayNoteLabel', + singular: () => i18n.t('Note'), + plural: () => i18n.t('Notes'), + singularLower: () => i18n.t('note'), + pluralLower: () => i18n.t('notes'), + }, + attribute: { + field: 'displayTrackedEntityAttributeLabel', + singular: () => i18n.t('Attribute'), + plural: () => i18n.t('Attributes'), + singularLower: () => i18n.t('attribute'), + pluralLower: () => i18n.t('attributes'), + }, + programStage: { + field: 'displayProgramStageLabel', + pluralField: 'displayProgramStagesLabel', + singular: () => i18n.t('Program stage'), + plural: () => i18n.t('Program stages'), + singularLower: () => i18n.t('program stage'), + pluralLower: () => i18n.t('program stages'), + }, + event: { + field: 'displayEventLabel', + pluralField: 'displayEventsLabel', + singular: () => i18n.t('Event'), + plural: () => i18n.t('Events'), + singularLower: () => i18n.t('event'), + pluralLower: () => i18n.t('events'), + }, + trackedEntityType: { + pluralField: 'displayTrackedEntityTypesLabel', + singular: () => i18n.t('Tracked entity type'), + plural: () => i18n.t('Tracked entity types'), + singularLower: () => i18n.t('tracked entity type'), + pluralLower: () => i18n.t('tracked entity types'), + }, +}; + +export type TermKey = keyof typeof TERMS; +export type CustomLabels = Record; +export type LabelOptions = { plural?: boolean, lowercase?: boolean }; + +const allFields: Array = Array.from( + new Set( + Object.values(TERMS) + .flatMap(term => [term.field, term.pluralField]) + .filter((field): field is string => Boolean(field)), + ), +); + +/** Copies the present custom-label values out of a cached metadata object. */ +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; + +/** + * Resolves a term against one or more label sources, checked in order (e.g. stage + * then program). Falls back to the translated default when no custom label is set. + * + * Plural rules: + * - term with a backend plural field → plural custom, else plural default + * - singular-only term → singular custom (reused), else plural default + */ +export const resolveLabel = ( + sources: LabelSource | Array, + key: TermKey, + { plural = false, lowercase = false }: LabelOptions = {}, +): string => { + const term = TERMS[key]; + const list = Array.isArray(sources) ? sources : [sources]; + const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); + + if (plural) { + const custom = term.pluralField ? pick(term.pluralField) : pick(term.field); + return custom ?? (lowercase ? term.pluralLower() : term.plural()); + } + return pick(term.field) ?? (lowercase ? term.singularLower() : term.singular()); +}; + +type WithLabels = { customLabels?: CustomLabels } | undefined | null; + +export const getProgramLabel = (program: WithLabels, key: TermKey, options?: LabelOptions): string => + resolveLabel(program?.customLabels, key, options); + +export const getStageLabel = ( + stage: WithLabels, + program: WithLabels, + key: TermKey, + options?: LabelOptions, +): string => resolveLabel([stage?.customLabels, program?.customLabels], key, options); + +export const getTrackedEntityTypeLabel = ( + trackedEntityType: WithLabels, + key: TermKey, + options?: LabelOptions, +): string => resolveLabel(trackedEntityType?.customLabels, key, options); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts deleted file mode 100644 index 9a0ef4141d..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/getCustomLabel.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getDefaultLabel } from './labels.const'; -import type { LabelKey } from './labels.const'; -import type { Program } from '../../Program/Program'; -import type { ProgramStage } from '../../Program/ProgramStage'; -import type { TrackedEntityType } from '../../TrackedEntityType/TrackedEntityType'; - -type Options = { lowercase?: boolean, plural?: boolean }; - -/** - * Resolves the configurable terminology label for a program-level term, falling - * back to the translated default when no custom label is configured. - */ -export const getProgramLabel = (program: Program | undefined | null, key: LabelKey, options?: Options): string => - program?.customLabels[key] ?? getDefaultLabel(key, options); - -/** - * Resolves a term that can be overridden at both stage and program level. - * Precedence: stage label → program label → translated default. - */ -export const getStageLabel = ( - stage: ProgramStage | undefined | null, - program: Program | undefined | null, - key: LabelKey, - options?: Options, -): string => - stage?.customLabels[key] ?? program?.customLabels[key] ?? getDefaultLabel(key, options); - -export const getTrackedEntityTypeLabel = ( - trackedEntityType: TrackedEntityType | undefined | null, - key: LabelKey, - options?: Options, -): string => - trackedEntityType?.customLabels[key] ?? getDefaultLabel(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 4f88fc8773..1a46e19385 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,10 @@ export { - labelKeys, - getDefaultLabel, - programLabelFields, - programStageLabelFields, - trackedEntityTypeLabelFields, -} from './labels.const'; -export type { LabelKey, CustomLabels } from './labels.const'; -export { getProgramLabel, getStageLabel, getTrackedEntityTypeLabel } from './getCustomLabel'; -export { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } from './useCustomLabel'; -export { applyCustomLabels } from './applyCustomLabels'; + TERMS, + resolveLabel, + extractCustomLabels, + getProgramLabel, + getStageLabel, + getTrackedEntityTypeLabel, +} from './customLabels'; +export type { TermKey, CustomLabels, LabelOptions } from './customLabels'; +export { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } from './useLabel'; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts deleted file mode 100644 index 2a9ca0b7c4..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/labels.const.ts +++ /dev/null @@ -1,131 +0,0 @@ -import i18n from '@dhis2/d2-i18n'; - -/** - * Keys for the configurable terminology labels (DHIS2-16581). - * A program / program stage / tracked entity type can override the default term - * shown in the UI; when no custom label is configured we fall back to the - * translated default (see getDefaultLabel). - */ -// Keys map 1:1 to the configurable label fields exposed by the backend / MMA. -// Singular-only terms (no backend plural) are pluralised at render time via the -// `plural` option on the resolver, which reuses the singular custom value. -export const labelKeys = Object.freeze({ - ENROLLMENT: 'ENROLLMENT', - ENROLLMENTS: 'ENROLLMENTS', - FOLLOW_UP: 'FOLLOW_UP', - ORG_UNIT: 'ORG_UNIT', - RELATIONSHIP: 'RELATIONSHIP', - NOTE: 'NOTE', - TRACKED_ENTITY_ATTRIBUTE: 'TRACKED_ENTITY_ATTRIBUTE', - PROGRAM_STAGE: 'PROGRAM_STAGE', - PROGRAM_STAGES: 'PROGRAM_STAGES', - EVENT: 'EVENT', - EVENTS: 'EVENTS', - TRACKED_ENTITY_TYPES: 'TRACKED_ENTITY_TYPES', -}); - -export type LabelKey = typeof labelKeys[keyof typeof labelKeys]; - -export type CustomLabels = { [key in LabelKey]?: string }; - -/** - * Maps each label key to the cached metadata field that holds the custom value. - * Used by the metadata factories to populate the sparse customLabels map. - */ -export const programLabelFields: { [key in LabelKey]?: string } = Object.freeze({ - [labelKeys.ENROLLMENT]: 'displayEnrollmentLabel', - [labelKeys.ENROLLMENTS]: 'displayEnrollmentsLabel', - [labelKeys.FOLLOW_UP]: 'displayFollowUpLabel', - [labelKeys.ORG_UNIT]: 'displayOrgUnitLabel', - [labelKeys.RELATIONSHIP]: 'displayRelationshipLabel', - [labelKeys.NOTE]: 'displayNoteLabel', - [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: 'displayTrackedEntityAttributeLabel', - [labelKeys.PROGRAM_STAGE]: 'displayProgramStageLabel', - [labelKeys.PROGRAM_STAGES]: 'displayProgramStagesLabel', - [labelKeys.EVENT]: 'displayEventLabel', - [labelKeys.EVENTS]: 'displayEventsLabel', -}); - -export const programStageLabelFields: { [key in LabelKey]?: string } = Object.freeze({ - [labelKeys.PROGRAM_STAGE]: 'displayProgramStageLabel', - [labelKeys.EVENT]: 'displayEventLabel', - [labelKeys.EVENTS]: 'displayEventsLabel', -}); - -export const trackedEntityTypeLabelFields: { [key in LabelKey]?: string } = Object.freeze({ - [labelKeys.TRACKED_ENTITY_TYPES]: 'displayTrackedEntityTypesLabel', -}); - -// Built lazily (inside getDefaultLabel) so i18n.t() resolves against the active locale. -const getCapitalizedDefaults = (): { [key in LabelKey]: string } => ({ - [labelKeys.ENROLLMENT]: i18n.t('Enrollment'), - [labelKeys.ENROLLMENTS]: i18n.t('Enrollments'), - [labelKeys.FOLLOW_UP]: i18n.t('Follow-up'), - [labelKeys.ORG_UNIT]: i18n.t('Organisation unit'), - [labelKeys.RELATIONSHIP]: i18n.t('Relationship'), - [labelKeys.NOTE]: i18n.t('Note'), - [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('Attribute'), - [labelKeys.PROGRAM_STAGE]: i18n.t('Program stage'), - [labelKeys.PROGRAM_STAGES]: i18n.t('Program stages'), - [labelKeys.EVENT]: i18n.t('Event'), - [labelKeys.EVENTS]: i18n.t('Events'), - [labelKeys.TRACKED_ENTITY_TYPES]: i18n.t('Tracked entity types'), -}); - -// Mid-sentence form (e.g. "Are you sure you want to delete this event?"). -const getLowercaseDefaults = (): { [key in LabelKey]: string } => ({ - [labelKeys.ENROLLMENT]: i18n.t('enrollment'), - [labelKeys.ENROLLMENTS]: i18n.t('enrollments'), - [labelKeys.FOLLOW_UP]: i18n.t('follow-up'), - [labelKeys.ORG_UNIT]: i18n.t('organisation unit'), - [labelKeys.RELATIONSHIP]: i18n.t('relationship'), - [labelKeys.NOTE]: i18n.t('note'), - [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('attribute'), - [labelKeys.PROGRAM_STAGE]: i18n.t('program stage'), - [labelKeys.PROGRAM_STAGES]: i18n.t('program stages'), - [labelKeys.EVENT]: i18n.t('event'), - [labelKeys.EVENTS]: i18n.t('events'), - [labelKeys.TRACKED_ENTITY_TYPES]: i18n.t('tracked entity types'), -}); - -// Plural defaults for singular-only terms (no backend plural field). Used when a -// plural UI slot has no custom label configured; if a custom label IS configured -// the singular custom value is used instead (see resolver). -const getCapitalizedPluralDefaults = (): { [key in LabelKey]?: string } => ({ - [labelKeys.FOLLOW_UP]: i18n.t('Follow-ups'), - [labelKeys.ORG_UNIT]: i18n.t('Organisation units'), - [labelKeys.RELATIONSHIP]: i18n.t('Relationships'), - [labelKeys.NOTE]: i18n.t('Notes'), - [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('Attributes'), -}); - -const getLowercasePluralDefaults = (): { [key in LabelKey]?: string } => ({ - [labelKeys.FOLLOW_UP]: i18n.t('follow-ups'), - [labelKeys.ORG_UNIT]: i18n.t('organisation units'), - [labelKeys.RELATIONSHIP]: i18n.t('relationships'), - [labelKeys.NOTE]: i18n.t('notes'), - [labelKeys.TRACKED_ENTITY_ATTRIBUTE]: i18n.t('attributes'), -}); - -type GetDefaultLabelOptions = { lowercase?: boolean, plural?: boolean }; - -/** - * Returns the translated default term for a label key. Custom labels (entered by - * the implementer) are used verbatim; only this default is translated/cased here. - * `plural` provides a plural default for singular-only terms (note, relationship, - * org unit, attribute, follow-up) which have no dedicated plural field. - */ -export const getDefaultLabel = ( - key: LabelKey, - { lowercase = false, plural = false }: GetDefaultLabelOptions = {}, -): string => { - if (plural) { - const pluralDefaults = lowercase ? getLowercasePluralDefaults() : getCapitalizedPluralDefaults(); - const pluralDefault = pluralDefaults[key]; - if (pluralDefault) { - return pluralDefault; - } - } - const defaults = lowercase ? getLowercaseDefaults() : getCapitalizedDefaults(); - return defaults[key]; -}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts deleted file mode 100644 index ea63d3d8e9..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useCustomLabel.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; -import { getProgramLabel, getStageLabel, getTrackedEntityTypeLabel } from './getCustomLabel'; -import type { LabelKey } from './labels.const'; - -type Options = { lowercase?: boolean, plural?: boolean }; - -/** - * Resolves a program-level configurable terminology label. Defaults to the - * currently selected program when no programId is passed. - */ -export const useProgramLabel = (key: LabelKey, programIdInput?: string, options?: Options): string => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const programId = programIdInput ?? currentProgramId; - const lowercase = options?.lowercase; - const plural = options?.plural; - return useMemo( - () => getProgramLabel(programId ? programCollection.get(programId) : undefined, key, { lowercase, plural }), - [programId, key, lowercase, plural], - ); -}; - -/** - * Resolves a term that can be overridden at both stage and program level - * (stage label → program label → default). Defaults to the currently selected - * program/stage when ids are not passed. - */ -export const useStageLabel = ( - key: LabelKey, - stageIdInput?: string, - programIdInput?: string, - options?: Options, -): string => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const currentStageId = useSelector(({ currentSelections }: any) => currentSelections.stageId); - const programId = programIdInput ?? currentProgramId; - const stageId = stageIdInput ?? currentStageId; - const lowercase = options?.lowercase; - const plural = options?.plural; - return useMemo(() => { - const program = programId ? programCollection.get(programId) : undefined; - const stage = program && stageId ? program.getStage(stageId) : undefined; - return getStageLabel(stage, program, key, { lowercase, plural }); - }, [programId, stageId, key, lowercase, plural]); -}; - -/** - * Resolves a tracked-entity-type-level configurable terminology label. Defaults - * to the currently selected tracked entity type when no id is passed. - */ -export const useTrackedEntityTypeLabel = (key: LabelKey, tetIdInput?: string, options?: Options): string => { - const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); - const tetId = tetIdInput ?? currentTetId; - const lowercase = options?.lowercase; - const plural = options?.plural; - return useMemo( - () => getTrackedEntityTypeLabel( - tetId ? trackedEntityTypesCollection.get(tetId) : undefined, key, { lowercase, plural }), - [tetId, key, lowercase, plural], - ); -}; 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..5e042452bb --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; +import { resolveLabel } from './customLabels'; +import type { TermKey, LabelOptions } from './customLabels'; + +type ProgramOptions = LabelOptions & { programId?: string }; +type StageOptions = LabelOptions & { programId?: string, stageId?: string }; +type TrackedEntityTypeOptions = LabelOptions & { tetId?: string }; + +/** Resolves a program-level term; defaults to the currently selected program. */ +export const useProgramLabel = (key: TermKey, { programId, plural, lowercase }: ProgramOptions = {}): string => { + const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); + const id = programId ?? currentProgramId; + return useMemo( + () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural, lowercase }), + [id, key, plural, lowercase], + ); +}; + +/** Resolves a term with stage → program fallback; defaults to the current program/stage. */ +export const useStageLabel = (key: TermKey, { programId, stageId, plural, lowercase }: StageOptions = {}): string => { + 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, lowercase }); + }, [pId, sId, key, plural, lowercase]); +}; + +/** Resolves a tracked-entity-type-level term; defaults to the current tracked entity type. */ +export const useTrackedEntityTypeLabel = ( + key: TermKey, + { tetId, plural, lowercase }: TrackedEntityTypeOptions = {}, +): string => { + const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); + const id = tetId ?? currentTetId; + return useMemo( + () => resolveLabel(id ? trackedEntityTypesCollection.get(id)?.customLabels : undefined, key, { plural, lowercase }), + [id, key, plural, lowercase], + ); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index 22b1de92bc..f4911dc22e 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -18,17 +18,14 @@ export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { - labelKeys, - getDefaultLabel, + TERMS, + resolveLabel, + extractCustomLabels, getProgramLabel, getStageLabel, getTrackedEntityTypeLabel, useProgramLabel, useStageLabel, useTrackedEntityTypeLabel, - applyCustomLabels, - programLabelFields, - programStageLabelFields, - trackedEntityTypeLabelFields, } from './customLabels'; -export type { LabelKey, CustomLabels } from './customLabels'; +export type { TermKey, 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 a97b364750..619d5d1594 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -40,17 +40,14 @@ export { getProgramThrowIfNotFound, getProgramAndStageForEventProgram, getEventProgramEventAccess, - labelKeys, - getDefaultLabel, + TERMS, + resolveLabel, + extractCustomLabels, getProgramLabel, getStageLabel, getTrackedEntityTypeLabel, useProgramLabel, useStageLabel, useTrackedEntityTypeLabel, - applyCustomLabels, - programLabelFields, - programStageLabelFields, - trackedEntityTypeLabelFields, } from './helpers'; -export type { LabelKey, CustomLabels } from './helpers'; +export type { TermKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts index 07ff10a249..1766348564 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts @@ -5,8 +5,7 @@ import { EventProgram, TrackerProgram, CategoryCombination, - applyCustomLabels, - programLabelFields, + extractCustomLabels, type TrackedEntityType, type Category, } from '../../../../metaData'; @@ -189,7 +188,7 @@ export class ProgramFactory { program.displayFrontPageList = cachedProgram.displayFrontPageList; program.onlyEnrollOnce = cachedProgram.onlyEnrollOnce; program.useFirstStageDuringRegistration = cachedProgram.useFirstStageDuringRegistration; - applyCustomLabels(program, cachedProgram, programLabelFields); + program.customLabels = extractCustomLabels(cachedProgram); return program; } 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 fbcf1a5e69..6a886b8f72 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 @@ -17,8 +17,7 @@ import { ProgramStage, RenderFoundation, CustomForm, - applyCustomLabels, - programStageLabelFields, + extractCustomLabels, } from '../../../../metaData'; import { buildIcon } from '../../../common/helpers'; import { isNonEmptyArray } from '../../../../utils/isNonEmptyArray'; @@ -249,7 +248,7 @@ export class ProgramStageFactory { camelCaseUppercaseString(cachedProgramStage.validationStrategy); }); _stage.icon = buildIcon(cachedProgramStage.style); - applyCustomLabels(_stage, cachedProgramStage, programStageLabelFields); + _stage.customLabels = extractCustomLabels(cachedProgramStage); }); const stageForm = stage.stageForm; 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 cbe29f1b09..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 @@ -1,8 +1,7 @@ /* eslint-disable no-underscore-dangle */ import { TrackedEntityType, - applyCustomLabels, - trackedEntityTypeLabelFields, + extractCustomLabels, } from '../../../../metaData'; import { DataElementFactory } from './DataElementFactory'; import { TeiRegistrationFactory } from './TeiRegistrationFactory'; @@ -88,7 +87,7 @@ export class TrackedEntityTypeFactory { o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - applyCustomLabels(o, cachedType, trackedEntityTypeLabelFields); + o.customLabels = extractCustomLabels(cachedType); }); if (cachedType.trackedEntityTypeAttributes) { From cb457dbc389ce57d1e92eb3a8b755c4f4453f3c5 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 13 Jun 2026 21:42:16 +0200 Subject: [PATCH 03/15] fix: remove lowercase option from label resolution functions --- i18n/en.pot | 49 +------------------ .../helpers/customLabels/customLabels.ts | 28 ++--------- .../metaData/helpers/customLabels/useLabel.ts | 21 ++++---- 3 files changed, 15 insertions(+), 83 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index f288bc6231..d6cdbb3a11 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-06-13T19:35:53.319Z\n" -"PO-Revision-Date: 2026-06-13T19:35:53.320Z\n" +"POT-Creation-Date: 2026-06-13T19:42:17.196Z\n" +"PO-Revision-Date: 2026-06-13T19:42:17.196Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2213,69 +2213,30 @@ msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" msgid "Enrollments" msgstr "Enrollments" -msgid "enrollments" -msgstr "enrollments" - msgid "Follow-ups" msgstr "Follow-ups" -msgid "follow-up" -msgstr "follow-up" - -msgid "follow-ups" -msgstr "follow-ups" - msgid "Organisation units" msgstr "Organisation units" -msgid "organisation unit" -msgstr "organisation unit" - -msgid "organisation units" -msgstr "organisation units" - msgid "Relationship" msgstr "Relationship" -msgid "relationship" -msgstr "relationship" - -msgid "relationships" -msgstr "relationships" - msgid "Note" msgstr "Note" -msgid "note" -msgstr "note" - -msgid "notes" -msgstr "notes" - msgid "Attribute" msgstr "Attribute" msgid "Attributes" msgstr "Attributes" -msgid "attribute" -msgstr "attribute" - -msgid "attributes" -msgstr "attributes" - msgid "Program stage" msgstr "Program stage" msgid "Program stages" msgstr "Program stages" -msgid "program stage" -msgstr "program stage" - -msgid "program stages" -msgstr "program stages" - msgid "Event" msgstr "Event" @@ -2288,12 +2249,6 @@ msgstr "Tracked entity type" msgid "Tracked entity types" msgstr "Tracked entity types" -msgid "tracked entity type" -msgstr "tracked entity type" - -msgid "tracked entity types" -msgstr "tracked entity types" - 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 38de0dfce0..4865932919 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -16,8 +16,6 @@ type TermDef = { pluralField?: string, singular: () => string, plural: () => string, - singularLower: () => string, - pluralLower: () => string, }; export const TERMS: { [key: string]: TermDef } = { @@ -26,72 +24,54 @@ export const TERMS: { [key: string]: TermDef } = { pluralField: 'displayEnrollmentsLabel', singular: () => i18n.t('Enrollment'), plural: () => i18n.t('Enrollments'), - singularLower: () => i18n.t('enrollment'), - pluralLower: () => i18n.t('enrollments'), }, followUp: { field: 'displayFollowUpLabel', singular: () => i18n.t('Follow-up'), plural: () => i18n.t('Follow-ups'), - singularLower: () => i18n.t('follow-up'), - pluralLower: () => i18n.t('follow-ups'), }, orgUnit: { field: 'displayOrgUnitLabel', singular: () => i18n.t('Organisation unit'), plural: () => i18n.t('Organisation units'), - singularLower: () => i18n.t('organisation unit'), - pluralLower: () => i18n.t('organisation units'), }, relationship: { field: 'displayRelationshipLabel', singular: () => i18n.t('Relationship'), plural: () => i18n.t('Relationships'), - singularLower: () => i18n.t('relationship'), - pluralLower: () => i18n.t('relationships'), }, note: { field: 'displayNoteLabel', singular: () => i18n.t('Note'), plural: () => i18n.t('Notes'), - singularLower: () => i18n.t('note'), - pluralLower: () => i18n.t('notes'), }, attribute: { field: 'displayTrackedEntityAttributeLabel', singular: () => i18n.t('Attribute'), plural: () => i18n.t('Attributes'), - singularLower: () => i18n.t('attribute'), - pluralLower: () => i18n.t('attributes'), }, programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel', singular: () => i18n.t('Program stage'), plural: () => i18n.t('Program stages'), - singularLower: () => i18n.t('program stage'), - pluralLower: () => i18n.t('program stages'), }, event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel', singular: () => i18n.t('Event'), plural: () => i18n.t('Events'), - singularLower: () => i18n.t('event'), - pluralLower: () => i18n.t('events'), }, trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel', singular: () => i18n.t('Tracked entity type'), plural: () => i18n.t('Tracked entity types'), - singularLower: () => i18n.t('tracked entity type'), - pluralLower: () => i18n.t('tracked entity types'), }, }; export type TermKey = keyof typeof TERMS; export type CustomLabels = Record; -export type LabelOptions = { plural?: boolean, lowercase?: boolean }; +export type LabelOptions = { plural?: boolean }; const allFields: Array = Array.from( new Set( @@ -125,7 +105,7 @@ type LabelSource = CustomLabels | undefined | null; export const resolveLabel = ( sources: LabelSource | Array, key: TermKey, - { plural = false, lowercase = false }: LabelOptions = {}, + { plural = false }: LabelOptions = {}, ): string => { const term = TERMS[key]; const list = Array.isArray(sources) ? sources : [sources]; @@ -133,9 +113,9 @@ export const resolveLabel = ( if (plural) { const custom = term.pluralField ? pick(term.pluralField) : pick(term.field); - return custom ?? (lowercase ? term.pluralLower() : term.plural()); + return custom ?? term.plural(); } - return pick(term.field) ?? (lowercase ? term.singularLower() : term.singular()); + return pick(term.field) ?? term.singular(); }; type WithLabels = { customLabels?: CustomLabels } | undefined | null; 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 5e042452bb..f8bd1815a4 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -9,17 +9,17 @@ type StageOptions = LabelOptions & { programId?: string, stageId?: string }; type TrackedEntityTypeOptions = LabelOptions & { tetId?: string }; /** Resolves a program-level term; defaults to the currently selected program. */ -export const useProgramLabel = (key: TermKey, { programId, plural, lowercase }: ProgramOptions = {}): string => { +export const useProgramLabel = (key: TermKey, { programId, plural }: ProgramOptions = {}): string => { const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); const id = programId ?? currentProgramId; return useMemo( - () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural, lowercase }), - [id, key, plural, lowercase], + () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural }), + [id, key, plural], ); }; /** Resolves a term with stage → program fallback; defaults to the current program/stage. */ -export const useStageLabel = (key: TermKey, { programId, stageId, plural, lowercase }: StageOptions = {}): string => { +export const useStageLabel = (key: TermKey, { programId, stageId, plural }: StageOptions = {}): string => { const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); const currentStageId = useSelector(({ currentSelections }: any) => currentSelections.stageId); const pId = programId ?? currentProgramId; @@ -27,19 +27,16 @@ export const useStageLabel = (key: TermKey, { programId, stageId, plural, lowerc 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, lowercase }); - }, [pId, sId, key, plural, lowercase]); + return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); + }, [pId, sId, key, plural]); }; /** Resolves a tracked-entity-type-level term; defaults to the current tracked entity type. */ -export const useTrackedEntityTypeLabel = ( - key: TermKey, - { tetId, plural, lowercase }: TrackedEntityTypeOptions = {}, -): string => { +export const useTrackedEntityTypeLabel = (key: TermKey, { tetId, plural }: TrackedEntityTypeOptions = {}): string => { const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); const id = tetId ?? currentTetId; return useMemo( - () => resolveLabel(id ? trackedEntityTypesCollection.get(id)?.customLabels : undefined, key, { plural, lowercase }), - [id, key, plural, lowercase], + () => resolveLabel(id ? trackedEntityTypesCollection.get(id)?.customLabels : undefined, key, { plural }), + [id, key, plural], ); }; From d3da46efd398e26aa97515c8b2100f4d371c97d5 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:31:01 +0200 Subject: [PATCH 04/15] feat: make resolver return the api custom string or undefined --- i18n/en.pot | 44 +------- .../helpers/customLabels/customLabels.ts | 102 +++++------------- .../metaData/helpers/customLabels/useLabel.ts | 15 +-- 3 files changed, 40 insertions(+), 121 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d6cdbb3a11..d8b43b4854 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-06-13T19:42:17.196Z\n" -"PO-Revision-Date: 2026-06-13T19:42:17.196Z\n" +"POT-Creation-Date: 2026-06-14T13:31:02.618Z\n" +"PO-Revision-Date: 2026-06-14T13:31:02.618Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2210,44 +2210,8 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" -msgid "Enrollments" -msgstr "Enrollments" - -msgid "Follow-ups" -msgstr "Follow-ups" - -msgid "Organisation units" -msgstr "Organisation units" - -msgid "Relationship" -msgstr "Relationship" - -msgid "Note" -msgstr "Note" - -msgid "Attribute" -msgstr "Attribute" - -msgid "Attributes" -msgstr "Attributes" - -msgid "Program stage" -msgstr "Program stage" - -msgid "Program stages" -msgstr "Program stages" - -msgid "Event" -msgstr "Event" - -msgid "Events" -msgstr "Events" - -msgid "Tracked entity type" -msgstr "Tracked entity type" - -msgid "Tracked entity types" -msgstr "Tracked entity types" +msgid "…" +msgstr "…" 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 4865932919..46bfb14443 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,73 +1,26 @@ -import i18n from '@dhis2/d2-i18n'; - /** * Configurable terminology (DHIS2-16581 + v43 plurals). * - * Single source of truth: each term declares the backend field(s) that hold its - * custom value and the translated defaults. The resolver returns the custom label - * if configured, otherwise the translated default. - * - * Default strings are wrapped in thunks (not evaluated at module load) so i18n.t() - * resolves against the active locale, and are written as literals so the i18n - * extractor can find them. + * Each term maps to the backend field(s) that hold the custom (translated) label. + * The resolver returns the configured custom string, or `undefined` when none is + * set — callers fall back to their existing default term (`?? i18n.t('…')`). */ type TermDef = { field?: string, pluralField?: string, - singular: () => string, - plural: () => string, }; -export const TERMS: { [key: string]: TermDef } = { - enrollment: { - field: 'displayEnrollmentLabel', - pluralField: 'displayEnrollmentsLabel', - singular: () => i18n.t('Enrollment'), - plural: () => i18n.t('Enrollments'), - }, - followUp: { - field: 'displayFollowUpLabel', - singular: () => i18n.t('Follow-up'), - plural: () => i18n.t('Follow-ups'), - }, - orgUnit: { - field: 'displayOrgUnitLabel', - singular: () => i18n.t('Organisation unit'), - plural: () => i18n.t('Organisation units'), - }, - relationship: { - field: 'displayRelationshipLabel', - singular: () => i18n.t('Relationship'), - plural: () => i18n.t('Relationships'), - }, - note: { - field: 'displayNoteLabel', - singular: () => i18n.t('Note'), - plural: () => i18n.t('Notes'), - }, - attribute: { - field: 'displayTrackedEntityAttributeLabel', - singular: () => i18n.t('Attribute'), - plural: () => i18n.t('Attributes'), - }, - programStage: { - field: 'displayProgramStageLabel', - pluralField: 'displayProgramStagesLabel', - singular: () => i18n.t('Program stage'), - plural: () => i18n.t('Program stages'), - }, - event: { - field: 'displayEventLabel', - pluralField: 'displayEventsLabel', - singular: () => i18n.t('Event'), - plural: () => i18n.t('Events'), - }, - trackedEntityType: { - pluralField: 'displayTrackedEntityTypesLabel', - singular: () => i18n.t('Tracked entity type'), - plural: () => i18n.t('Tracked entity types'), - }, -}; +export const TERMS = { + 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' }, +} as const satisfies { [key: string]: TermDef }; export type TermKey = keyof typeof TERMS; export type CustomLabels = Record; @@ -76,7 +29,7 @@ export type LabelOptions = { plural?: boolean }; const allFields: Array = Array.from( new Set( Object.values(TERMS) - .flatMap(term => [term.field, term.pluralField]) + .flatMap((term: TermDef) => [term.field, term.pluralField]) .filter((field): field is string => Boolean(field)), ), ); @@ -95,32 +48,31 @@ export const extractCustomLabels = (cached: Record): CustomLabels = type LabelSource = CustomLabels | undefined | null; /** - * Resolves a term against one or more label sources, checked in order (e.g. stage - * then program). Falls back to the translated default when no custom label is set. + * Resolves the custom label for a term against one or more sources, checked in + * order (e.g. stage then program). Returns `undefined` when no custom label is + * configured, so the caller keeps its own default term. * - * Plural rules: - * - term with a backend plural field → plural custom, else plural default - * - singular-only term → singular custom (reused), else plural default + * For a plural slot: a term with a backend plural field uses that field; a + * singular-only term reuses its singular custom value. */ export const resolveLabel = ( sources: LabelSource | Array, key: TermKey, { plural = false }: LabelOptions = {}, -): string => { - const term = TERMS[key]; +): string | undefined => { + const term: TermDef = TERMS[key]; const list = Array.isArray(sources) ? sources : [sources]; const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); if (plural) { - const custom = term.pluralField ? pick(term.pluralField) : pick(term.field); - return custom ?? term.plural(); + return term.pluralField ? pick(term.pluralField) : pick(term.field); } - return pick(term.field) ?? term.singular(); + return pick(term.field); }; type WithLabels = { customLabels?: CustomLabels } | undefined | null; -export const getProgramLabel = (program: WithLabels, key: TermKey, options?: LabelOptions): string => +export const getProgramLabel = (program: WithLabels, key: TermKey, options?: LabelOptions): string | undefined => resolveLabel(program?.customLabels, key, options); export const getStageLabel = ( @@ -128,10 +80,10 @@ export const getStageLabel = ( program: WithLabels, key: TermKey, options?: LabelOptions, -): string => resolveLabel([stage?.customLabels, program?.customLabels], key, options); +): string | undefined => resolveLabel([stage?.customLabels, program?.customLabels], key, options); export const getTrackedEntityTypeLabel = ( trackedEntityType: WithLabels, key: TermKey, options?: LabelOptions, -): string => resolveLabel(trackedEntityType?.customLabels, key, options); +): string | undefined => resolveLabel(trackedEntityType?.customLabels, key, options); 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 f8bd1815a4..3d6c0118f5 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -8,8 +8,8 @@ type ProgramOptions = LabelOptions & { programId?: string }; type StageOptions = LabelOptions & { programId?: string, stageId?: string }; type TrackedEntityTypeOptions = LabelOptions & { tetId?: string }; -/** Resolves a program-level term; defaults to the currently selected program. */ -export const useProgramLabel = (key: TermKey, { programId, plural }: ProgramOptions = {}): string => { +/** Resolves a program-level custom term; defaults to the currently selected program. */ +export const useProgramLabel = (key: TermKey, { programId, plural }: ProgramOptions = {}): string | undefined => { const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); const id = programId ?? currentProgramId; return useMemo( @@ -18,8 +18,8 @@ export const useProgramLabel = (key: TermKey, { programId, plural }: ProgramOpti ); }; -/** Resolves a term with stage → program fallback; defaults to the current program/stage. */ -export const useStageLabel = (key: TermKey, { programId, stageId, plural }: StageOptions = {}): string => { +/** Resolves a custom term with stage → program fallback; defaults to the current program/stage. */ +export const useStageLabel = (key: TermKey, { programId, stageId, plural }: StageOptions = {}): string | undefined => { const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); const currentStageId = useSelector(({ currentSelections }: any) => currentSelections.stageId); const pId = programId ?? currentProgramId; @@ -31,8 +31,11 @@ export const useStageLabel = (key: TermKey, { programId, stageId, plural }: Stag }, [pId, sId, key, plural]); }; -/** Resolves a tracked-entity-type-level term; defaults to the current tracked entity type. */ -export const useTrackedEntityTypeLabel = (key: TermKey, { tetId, plural }: TrackedEntityTypeOptions = {}): string => { +/** Resolves a tracked-entity-type-level custom term; defaults to the current tracked entity type. */ +export const useTrackedEntityTypeLabel = ( + key: TermKey, + { tetId, plural }: TrackedEntityTypeOptions = {}, +): string | undefined => { const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); const id = tetId ?? currentTetId; return useMemo( From 112cd38ba2fea97c09b66d17dc5180c45f63563d Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:06:19 +0200 Subject: [PATCH 05/15] feat: rename terms const --- .../metaData/helpers/customLabels/customLabels.ts | 8 ++++---- .../capture-core/metaData/helpers/customLabels/index.ts | 2 +- src/core_modules/capture-core/metaData/helpers/index.ts | 2 +- src/core_modules/capture-core/metaData/index.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) 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 46bfb14443..580c16023c 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -10,7 +10,7 @@ type TermDef = { pluralField?: string, }; -export const TERMS = { +export const CUSTOM_LABEL_FIELDS = { enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, followUp: { field: 'displayFollowUpLabel' }, orgUnit: { field: 'displayOrgUnitLabel' }, @@ -22,13 +22,13 @@ export const TERMS = { trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel' }, } as const satisfies { [key: string]: TermDef }; -export type TermKey = keyof typeof TERMS; +export type TermKey = keyof typeof CUSTOM_LABEL_FIELDS; export type CustomLabels = Record; export type LabelOptions = { plural?: boolean }; const allFields: Array = Array.from( new Set( - Object.values(TERMS) + Object.values(CUSTOM_LABEL_FIELDS) .flatMap((term: TermDef) => [term.field, term.pluralField]) .filter((field): field is string => Boolean(field)), ), @@ -60,7 +60,7 @@ export const resolveLabel = ( key: TermKey, { plural = false }: LabelOptions = {}, ): string | undefined => { - const term: TermDef = TERMS[key]; + const term: TermDef = CUSTOM_LABEL_FIELDS[key]; const list = Array.isArray(sources) ? sources : [sources]; const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); 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 1a46e19385..d1b0c77c08 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -1,5 +1,5 @@ export { - TERMS, + CUSTOM_LABEL_FIELDS, resolveLabel, extractCustomLabels, getProgramLabel, diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index f4911dc22e..59cc2f3f15 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -18,7 +18,7 @@ export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { - TERMS, + CUSTOM_LABEL_FIELDS, resolveLabel, extractCustomLabels, getProgramLabel, diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 619d5d1594..4413001843 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -40,7 +40,7 @@ export { getProgramThrowIfNotFound, getProgramAndStageForEventProgram, getEventProgramEventAccess, - TERMS, + CUSTOM_LABEL_FIELDS, resolveLabel, extractCustomLabels, getProgramLabel, From 506eda42a1f5c83ca9e8fc57073408f768bea6c5 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:14:13 +0200 Subject: [PATCH 06/15] feat: custom terminology labels --- i18n/en.pot | 940 ++++++++++-------- .../hooks/useOriginLabel.ts | 27 +- .../EnrollmentBreadcrumb.tsx | 14 +- .../hooks/useWorkingListLabel.ts | 17 +- .../EventBreadcrumb/EventBreadcrumb.tsx | 7 +- .../hooks/useWorkingListLabel.ts | 6 +- .../CardList/CardListButtons.component.tsx | 5 +- .../CardList/CardListItem.component.tsx | 7 +- .../hooks/useDataEntrySections.ts | 11 +- .../RecentlyAddedEventsList.component.tsx | 13 +- .../CompleteModal/CompleteModal.component.tsx | 63 +- .../CompleteModal/CompleteModal.container.tsx | 10 +- .../CompleteModal/completeModal.types.ts | 3 + .../LockedSelector/LockedSelector.epics.ts | 6 +- .../components/Notes/Notes.component.tsx | 13 +- .../EnrollmentPageDefault.container.tsx | 7 +- .../EnrollmentQuickActions.component.tsx | 8 +- .../Enrollment/MissingMessage.component.tsx | 37 +- .../Pages/Enrollment/TopBar.container.tsx | 4 +- ...nrollmentAddEventPageDefault.component.tsx | 7 +- ...nrollmentAddEventPageDefault.container.tsx | 5 +- .../NewEventWorkspace.component.tsx | 10 +- .../WidgetStageHeader.component.tsx | 14 +- .../ProgramStageSelector.component.tsx | 103 +- .../ProgramStageSelector.container.tsx | 12 +- .../TopBar/TopBar.component.tsx | 7 +- .../EnrollmentEditEventPage.component.tsx | 158 +-- .../EnrollmentEditEvent/TopBar.container.tsx | 8 +- ...idCategoryCombinationForOrgUnitMessage.tsx | 22 +- .../WithoutOrgUnitSelectedMessage.tsx | 7 +- .../Header/EventWorkingListsInitHeader.tsx | 36 +- .../Pages/New/NewPage.component.tsx | 6 +- .../RegistrationDataEntry.component.tsx | 6 +- .../DataEntryEnrollment.component.tsx | 5 +- .../StageEventHeader.component.tsx | 47 +- .../EventDetailsSection.component.tsx | 11 +- ...wEventNewRelationshipWrapper.component.tsx | 24 +- .../ViewEventNewRelationshipWrapper.types.ts | 2 + .../ViewEvent/ViewEventPage.component.tsx | 10 +- .../DataEntryEnrollment.component.tsx | 5 +- .../TeiSearch/TeiSearch.component.tsx | 5 +- .../WidgetEventEditWrapper.tsx | 9 +- .../ReadOnlyBadge/ReadOnlyBadge.tsx | 53 +- .../QuickSelector/Program/ProgramList.tsx | 5 +- .../SearchBox/SearchBox.component.tsx | 7 +- .../SearchForm/SearchForm.component.tsx | 5 +- .../SearchResults/SearchResults.component.tsx | 8 +- .../SearchStatus/SearchStatus.component.tsx | 6 +- .../TopBarActions/TopBarActions.component.tsx | 5 +- .../TrackedEntityTypeSelector.component.tsx | 11 +- .../WidgetAssignee/DisplayMode.component.tsx | 16 +- .../WidgetBreakingTheGlass.component.tsx | 17 +- .../Actions/Actions.component.tsx | 7 +- .../Actions/AddNew/AddNew.component.tsx | 8 +- .../CompleteModal/CompleteModal.component.tsx | 178 ++-- .../Actions/Delete/Delete.component.tsx | 27 +- .../Actions/Followup/Followup.component.tsx | 20 +- .../InfoBoxes/InfoBoxes.component.tsx | 16 +- .../TransferModal/TransferModal.component.tsx | 9 +- .../WidgetEnrollment.component.tsx | 17 +- .../OrgUnitFetcher.component.tsx | 7 +- .../WidgetEnrollmentNote.component.tsx | 21 +- .../WidgetHeader/WidgetHeader.container.tsx | 8 +- .../WidgetEventNote.component.tsx | 21 +- .../InfoBox/InfoBox.component.tsx | 24 +- .../ScheduleOrgUnit.component.tsx | 4 +- .../ScheduleText/ScheduleText.component.tsx | 26 +- .../WidgetEventSchedule.component.tsx | 19 +- .../WidgetNote/NoteSection/NoteSection.tsx | 10 +- .../DataEntry/DataEntry.component.tsx | 102 +- .../DeleteModal/DeleteModal.component.tsx | 7 +- .../WidgetProfile/WidgetProfile.component.tsx | 10 +- .../OrgUnitSelectorForRelatedStages.tsx | 4 +- .../LinkToExisting.component.tsx | 13 +- .../RelatedStagesActions.component.tsx | 29 +- .../WidgetRelatedStages.container.tsx | 4 +- .../hooks/useAddEventWithRelationship.ts | 6 +- .../StageCreateNewButton.tsx | 19 +- .../DeleteActionButton/DeleteActionButton.tsx | 6 +- .../DeleteActionModal/DeleteActionModal.tsx | 14 +- .../EventRow/SkipAction/SkipAction.tsx | 7 +- .../StageDetail/StageDetail.component.tsx | 13 +- .../StageOverview/StageOverview.component.tsx | 14 +- .../Stages/Stages.component.tsx | 7 +- .../WidgetStagesAndEvents.component.tsx | 9 +- .../Modal/UnlinkAndDeleteModal.tsx | 27 +- .../OverflowMenu/Modal/UnlinkModal.tsx | 28 +- .../OverflowMenu/OverflowMenu.component.tsx | 28 +- .../WidgetWrapper/WidgetWrapper.container.tsx | 10 +- .../Breadcrumbs/Breadcrumbs.component.tsx | 5 +- ...NewTrackedEntityRelationship.component.tsx | 4 +- ...NewTrackedEntityRelationship.container.tsx | 4 +- .../hooks/useAddRelationship.ts | 4 +- ...getTrackedEntityRelationship.component.tsx | 9 +- .../DeleteRelationship/DeleteRelationship.tsx | 10 +- .../useDeleteRelationship.ts | 6 +- .../RelationshipsWidget.component.tsx | 8 +- .../RowMenuSetup/DeleteEventModal.tsx | 10 +- ...ventWorkingListsRowMenuSetup.component.tsx | 6 +- .../Actions/CompleteAction/CompleteAction.tsx | 23 +- .../hooks/useBulkCompleteEvents.ts | 4 +- .../Actions/DeleteAction/DeleteAction.tsx | 14 +- .../useDefaultColumnConfig.ts | 16 +- .../Setup/hooks/useDefaultColumnConfig.ts | 40 +- .../Setup/hooks/useFiltersOnly.ts | 17 +- .../Setup/hooks/useProgramStageFilters.ts | 14 +- .../Setup/hooks/useStaticTemplates.ts | 15 +- .../Actions/CompleteAction/CompleteAction.tsx | 31 +- .../hooks/useCompleteBulkEnrollments.ts | 12 +- .../DeleteEnrollmentsAction.tsx | 10 +- .../EnrollmentDeleteModal.tsx | 36 +- .../hooks/useDeleteEnrollments.ts | 5 +- 112 files changed, 1878 insertions(+), 1054 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d8b43b4854..4f971181a4 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-06-14T13:31:02.618Z\n" -"PO-Revision-Date: 2026-06-14T13:31:02.618Z\n" +"POT-Creation-Date: 2026-06-14T18:14:14.730Z\n" +"PO-Revision-Date: 2026-06-14T18:14:14.731Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -42,47 +42,62 @@ msgstr "Bulk data entry" msgid "Program overview" msgstr "Program overview" -msgid "Active enrollments" -msgstr "Active enrollments" +msgid "enrollments" +msgstr "enrollments" -msgid "Completed enrollments" -msgstr "Completed enrollments" +msgid "Active {{enrollments}}" +msgstr "Active {{enrollments}}" -msgid "Cancelled enrollments" -msgstr "Cancelled enrollments" +msgid "Completed {{enrollments}}" +msgstr "Completed {{enrollments}}" + +msgid "Cancelled {{enrollments}}" +msgstr "Cancelled {{enrollments}}" msgid "Search" msgstr "Search" -msgid "Enrollment dashboard" -msgstr "Enrollment dashboard" +msgid "Enrollment" +msgstr "Enrollment" + +msgid "event" +msgstr "event" -msgid "View event" -msgstr "View event" +msgid "{{enrollment}} dashboard" +msgstr "{{enrollment}} dashboard" -msgid "Edit event" -msgstr "Edit event" +msgid "View {{event}}" +msgstr "View {{event}}" -msgid "New event" -msgstr "New event" +msgid "Edit {{event}}" +msgstr "Edit {{event}}" + +msgid "New {{event}}" +msgstr "New {{event}}" msgid "Loading..." msgstr "Loading..." -msgid "Event list" -msgstr "Event list" +msgid "Event" +msgstr "Event" + +msgid "{{event}} list" +msgstr "{{event}} list" msgid "More" msgstr "More" +msgid "enrollment" +msgstr "enrollment" + msgid "View {{programName}} dashboard" msgstr "View {{programName}} dashboard" msgid "View dashboard" msgstr "View dashboard" -msgid "View active enrollment" -msgstr "View active enrollment" +msgid "View active {{enrollment}}" +msgstr "View active {{enrollment}}" msgid "Re-enroll in {{programName}}" msgstr "Re-enroll in {{programName}}" @@ -90,17 +105,17 @@ msgstr "Re-enroll in {{programName}}" msgid "Re-enroll" msgstr "Re-enroll" +msgid "Organisation unit" +msgstr "Organisation unit" + msgid "Enrolled" msgstr "Enrolled" msgid "Previously enrolled" msgstr "Previously enrolled" -msgid "Organisation unit" -msgstr "Organisation unit" - -msgid "Date of enrollment" -msgstr "Date of enrollment" +msgid "Date of {{enrollment}}" +msgstr "Date of {{enrollment}}" msgid "Last updated" msgstr "Last updated" @@ -141,9 +156,6 @@ msgstr "Area" msgid "Coordinate" msgstr "Coordinate" -msgid "Enrollment" -msgstr "Enrollment" - msgid "Complete event" msgstr "Complete event" @@ -276,13 +288,16 @@ 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 "events" +msgstr "events" -msgid "No events added" -msgstr "No events added" +msgid "No {{eventLabel}} added" +msgstr "No {{eventLabel}} added" + +msgid "{{count}} {{eventLabel}} added" +msgid_plural "{{count}} {{eventLabel}} added" +msgstr[0] "{{count}} {{eventLabel}} added" +msgstr[1] "{{count}} {{eventLabel}} added" msgid "New event relationship" msgstr "New event relationship" @@ -318,9 +333,6 @@ msgstr "Saving a {{trackedEntityName}}" msgid "without" msgstr "without" -msgid "enrollment" -msgstr "enrollment" - msgid "in" msgstr "in" @@ -333,25 +345,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 " +"{{eventPluralLabel}} as well?" +msgstr "" +"Would you like to complete the {{enrollmentLabel}} and all active " +"{{eventPluralLabel}} 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 "{{count}} {{eventLabel}} in {{programStageName}}" +msgid_plural "{{count}} {{eventLabel}} in {{programStageName}}" +msgstr[0] "{{count}} {{eventLabel}} in {{programStageName}}" +msgstr[1] "{{count}} {{eventLabel}} in {{programStageName}}" -msgid "Yes, complete enrollment and events" -msgstr "Yes, complete enrollment and events" +msgid "Yes, complete {{enrollmentLabel}} and {{eventPluralLabel}}" +msgstr "Yes, complete {{enrollmentLabel}} and {{eventPluralLabel}}" -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" @@ -694,14 +710,20 @@ 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 "organisation unit" +msgstr "organisation unit" -msgid "Add note" -msgstr "Add note" +msgid "Selected program is invalid for selected {{orgUnit}}" +msgstr "Selected program is invalid for selected {{orgUnit}}" -msgid "Write note" -msgstr "Write note" +msgid "note" +msgstr "note" + +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" @@ -712,11 +734,17 @@ 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" -msgid "Schedule an event" -msgstr "Schedule an event" +msgid "Schedule {{event}}" +msgstr "Schedule {{event}}" msgid "Make referral" msgstr "Make referral" @@ -724,27 +752,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 {{enrollment}} id {{enrollmentId}}." +msgstr "Invalid {{enrollment}} id {{enrollmentId}}." -msgid "Choose an enrollment to view the dashboard." -msgstr "Choose an enrollment to view the dashboard." +msgid "Choose this {{enrollment}} to view the dashboard." +msgstr "Choose this {{enrollment}} 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 {{enrollments}} for " "{{teiDisplayName}}" msgstr "" -"Choose a program to add new or see existing enrollments for " +"Choose a program to add new or see existing {{enrollments}} 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 {{enrollments}}." +msgstr "There are no active {{enrollments}}." -msgid "Add new enrollment for {{teiDisplayName}} in this program." -msgstr "Add new enrollment for {{teiDisplayName}} in this program." +msgid "Add new {{enrollment}} for {{teiDisplayName}} in this program." +msgstr "Add new {{enrollment}} for {{teiDisplayName}} in this program." msgid "" "You do not have permissions to access to this program, registering unit or " @@ -761,19 +789,21 @@ 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}} " +"{{enrollment}}. " 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}} " +"{{enrollment}}. " 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 "{{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." +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." @@ -805,11 +835,26 @@ 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 "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 "Program stage" +msgstr "Program stage" + +msgid "{{programStage}} is invalid" +msgstr "{{programStage}} is invalid" + +msgid "Stage" +msgstr "Stage" -msgid "Stage not found" -msgstr "Stage not found" +msgid "{{programStage}} not found" +msgstr "{{programStage}} not found" + +msgid "You don't have access to create a new {{event}} in the current selections" +msgstr "You don't have access to create a new {{event}} in the current selections" msgid "Report" msgstr "Report" @@ -820,26 +865,26 @@ 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" +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" -msgid "Choose a stage for a new event" -msgstr "Choose a stage for a new event" +msgid "stage" +msgstr "stage" -msgid "Program Stages could not be loaded" -msgstr "Program Stages could not be loaded" +msgid "Program Stages" +msgstr "Program Stages" -msgid "Stage" -msgstr "Stage" +msgid "Choose the {{programStage}} for a new {{event}}" +msgstr "Choose the {{programStage}} for a new {{event}}" -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 "{{programStages}} could not be loaded" +msgstr "{{programStages}} could not be loaded" + +msgid "The category option is not valid for the selected {{orgUnit}}." +msgstr "The category option is not valid for the selected {{orgUnit}}." msgid "Please select a valid combination." msgstr "Please select a valid combination." @@ -866,17 +911,17 @@ 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 "Please select {{orgUnit}}" +msgstr "Please select {{orgUnit}}" -msgid "See working list without organisation unit" -msgstr "See working list without organisation unit" +msgid "See working list without {{orgUnit}}" +msgstr "See working list without {{orgUnit}}" msgid "Search for a {{trackedEntityName}}" msgstr "Search for a {{trackedEntityName}}" -msgid "Registered events" -msgstr "Registered events" +msgid "Registered {{events}}" +msgstr "Registered {{events}}" msgid "" "You don't have access to create a {{trackedEntityName}} in the current " @@ -885,8 +930,8 @@ 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 {{orgUnit}} to start reporting" +msgstr "Choose {{orgUnit}} to start reporting" msgid "Choose the {{missingCategories}} to start reporting" msgstr "Choose the {{missingCategories}} to start reporting" @@ -900,8 +945,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 {{enrollment}} in program{{escape}} {{programName}}" +msgstr "New {{enrollment}} in program{{escape}} {{programName}}" msgid "Save {{trackedEntityTypeName}}" msgstr "Save {{trackedEntityTypeName}}" @@ -909,8 +954,11 @@ 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 "relationship" +msgstr "relationship" + +msgid "Enter details now is not available when creating this {{relationship}}" +msgstr "Enter details now is not available when creating this {{relationship}}" msgid "Save new {{trackedEntityTypeName}} and link" msgstr "Save new {{trackedEntityTypeName}} and link" @@ -960,17 +1008,20 @@ 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 "{{event}} details" +msgstr "{{event}} details" + +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 "" "Leaving this page will discard any selections you made for a new " @@ -1018,11 +1069,14 @@ msgstr "Organisation unit scope" msgid "Selected program" msgstr "Selected program" +msgid "attributes" +msgstr "attributes" + msgid "Search {{uniqueAttrName}}" msgstr "Search {{uniqueAttrName}}" -msgid "Search by attributes" -msgstr "Search by attributes" +msgid "Search by {{attributes}}" +msgstr "Search by {{attributes}}" msgid "Search {{attributeName}}" msgstr "Search {{attributeName}}" @@ -1044,11 +1098,14 @@ msgstr "Back to search" msgid "Search form is missing. See log for details" msgstr "Search form is missing. See log for details" +msgid "Search by attributes" +msgstr "Search by attributes" + 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 {{enrollment}} {{event}} data could not be found" +msgstr "The {{enrollment}} {{event}} data could not be found" msgid "Loading" msgstr "Loading" @@ -1071,8 +1128,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 {{enrollment}}" +msgstr "You only have view access to this {{enrollment}}" msgid "You only have view access to this program" msgstr "You only have view access to this program" @@ -1080,20 +1137,29 @@ msgstr "You only have view access to this program" msgid "You only have view access to this {{trackedEntityName}}" 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 this {{trackedEntityType}}" +msgstr "You only have view access to this {{trackedEntityType}}" -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 these {{programStages}}" +msgstr "You only have view access to these {{programStages}}" -msgid "You only have view access to this program stage" -msgstr "You only have view access to this program stage" +msgid "You only have view access to this {{programStage}}" +msgstr "You only have view access to this {{programStage}}" -msgid "This event is outside the valid editing period" -msgstr "This event is outside the valid editing period" +msgid "This {{event}} is outside the valid editing period" +msgstr "This {{event}} is outside the valid editing period" -msgid "This event has been completed" -msgstr "This event has been completed" +msgid "This {{event}} has been completed" +msgstr "This {{event}} has been completed" + +msgid "tracked entity type" +msgstr "tracked entity type" + +msgid "program stage" +msgstr "program stage" + +msgid "program stages" +msgstr "program stages" msgid "View only - {{message}}" msgstr "View only - {{message}}" @@ -1131,8 +1197,8 @@ 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 "Some programs are being filtered by the chosen {{orgUnit}}" +msgstr "Some programs are being filtered by the chosen {{orgUnit}}" msgid "Show all programs" msgstr "Show all programs" @@ -1160,10 +1226,10 @@ 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 " +"Try selecting a different {{trackedEntityType}}, or try searching in a " "program by choosing one from the top bar." msgstr "" -"Try selecting a different tracked entity type, or try searching in a " +"Try selecting a different {{trackedEntityType}}, or try searching in a " "program by choosing one from the top bar." msgid "Search for {{titleText}}" @@ -1181,8 +1247,8 @@ 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 "No searchable {{attributes}} for {{trackedEntityName}}" +msgstr "No searchable {{attributes}} for {{trackedEntityName}}" msgid "Search in all programs" msgstr "Search in all programs" @@ -1233,11 +1299,11 @@ 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." +"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." +"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" @@ -1267,8 +1333,8 @@ 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 "Create new {{event}}" +msgstr "Create new {{event}}" msgid "Search for a {{trackedEntityName}} in {{programName}}" msgstr "Search for a {{trackedEntityName}} in {{programName}}" @@ -1276,14 +1342,17 @@ msgstr "Search for a {{trackedEntityName}} in {{programName}}" msgid "Search..." msgstr "Search..." -msgid "Select tracked entity type" -msgstr "Select tracked entity type" +msgid "tracked entity types" +msgstr "tracked entity types" + +msgid "Select {{trackedEntityType}}" +msgstr "Select {{trackedEntityType}}" -msgid "Type to filter tracked entity types" -msgstr "Type to filter tracked entity types" +msgid "Type to filter {{trackedEntityTypes}}" +msgstr "Type to filter {{trackedEntityTypes}}" -msgid "No tracked entity types available" -msgstr "No tracked entity types available" +msgid "No {{trackedEntityTypes}} available" +msgstr "No {{trackedEntityTypes}} available" msgid "Assigned to" msgstr "Assigned to" @@ -1291,8 +1360,8 @@ msgstr "Assigned to" msgid "Edit" msgstr "Edit" -msgid "No one is assigned to this event" -msgstr "No one is assigned to this event" +msgid "No one is assigned to this {{event}}" +msgstr "No one is assigned to this {{event}}" msgid "Assign" msgstr "Assign" @@ -1310,14 +1379,14 @@ msgstr "" "Describe the reason you are checking for enrollments in this protected " "program" -msgid "Check for enrollments" -msgstr "Check for enrollments" +msgid "Check for {{enrollments}}" +msgstr "Check for {{enrollments}}" msgid "" -"You must provide a reason to check for enrollments in this protected " +"You must provide a reason to check for {{enrollments}} in this protected " "program." msgstr "" -"You must provide a reason to check for enrollments in this protected " +"You must provide a reason to check for {{enrollments}} in this protected " "program." msgid "All activity will be logged." @@ -1329,14 +1398,14 @@ msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" -msgid "Enrollment actions" -msgstr "Enrollment actions" +msgid "{{enrollment}} actions" +msgstr "{{enrollment}} 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 "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" @@ -1350,26 +1419,50 @@ 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 "Complete {{enrollment}}" +msgstr "Complete {{enrollment}}" + +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 "{{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 "Complete {{enrollment}} only" +msgstr "Complete {{enrollment}} only" + +msgid "You do not have access to delete this {{enrollment}}" +msgstr "You do not have access to delete this {{enrollment}}" + +msgid "Delete {{enrollment}}" +msgstr "Delete {{enrollment}}" -msgid "Delete enrollment" -msgstr "Delete enrollment" +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 enrollment?" -msgstr "Are you sure you want to delete this enrollment?" +msgid "This will permanently remove the current {{enrollment}}." +msgstr "This will permanently remove the current {{enrollment}}." -msgid "This will permanently remove the current enrollment." -msgstr "This will permanently remove the current enrollment." +msgid "Yes, delete {{enrollment}}." +msgstr "Yes, delete {{enrollment}}." -msgid "Yes, delete enrollment." -msgstr "Yes, delete enrollment." +msgid "follow-up" +msgstr "follow-up" -msgid "Remove mark for follow-up" -msgstr "Remove mark for follow-up" +msgid "Remove mark for {{followUp}}" +msgstr "Remove mark for {{followUp}}" -msgid "Mark for follow-up" -msgstr "Mark for follow-up" +msgid "Mark for {{followUp}}" +msgstr "Mark for {{followUp}}" msgid "Transfer" msgstr "Transfer" @@ -1408,27 +1501,27 @@ msgid "Set area" msgstr "Set area" msgid "" -"Transferring enrollment ownership from {{ownerOrgUnit}} to " +"Transferring {{enrollment}} ownership from {{ownerOrgUnit}} to " "{{newOrgUnit}}{{escape}}" msgstr "" -"Transferring enrollment ownership from {{ownerOrgUnit}} to " +"Transferring {{enrollment}} ownership from {{ownerOrgUnit}} to " "{{newOrgUnit}}{{escape}}" msgid "" -"You will lose access to the enrollment when transferring ownership to " +"You will lose access to the {{enrollment}} when transferring ownership to " "{{organisationUnit}}." msgstr "" -"You will lose access to the enrollment when transferring ownership to " +"You will lose access to the {{enrollment}} when transferring ownership to " "{{organisationUnit}}." msgid "Transfer Ownership" msgstr "Transfer Ownership" msgid "" -"Choose the organisation unit to which enrollment ownership should be " +"Choose the {{orgUnit}} to which {{enrollment}} ownership should be " "transferred." msgstr "" -"Choose the organisation unit to which enrollment ownership should be " +"Choose the {{orgUnit}} to which {{enrollment}} ownership should be " "transferred." msgid "Enrollment date" @@ -1437,12 +1530,12 @@ 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" +msgid "{{enrollment}} widget could not be loaded. Please try again later" +msgstr "{{enrollment}} widget could not be loaded. Please try again later" + msgid "Started at{{escape}}" msgstr "Started at{{escape}}" @@ -1461,8 +1554,8 @@ 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." +msgid "{{orgUnit}} could not be retrieved. Please try again later." +msgstr "{{orgUnit}} could not be retrieved. Please try again later." msgid "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" @@ -1473,14 +1566,17 @@ msgstr "Saving to {{stageName}} for {{programName}}" msgid "program or stage is invalid" msgstr "program or stage is invalid" -msgid "Notes about this enrollment" -msgstr "Notes about this enrollment" +msgid "notes" +msgstr "notes" -msgid "Write a note about this enrollment" -msgstr "Write a note about this enrollment" +msgid "{{notes}} about this {{enrollment}}" +msgstr "{{notes}} about this {{enrollment}}" -msgid "This enrollment doesn't have any notes" -msgstr "This enrollment doesn't have any notes" +msgid "Write a note about this {{enrollment}}" +msgstr "Write a note about this {{enrollment}}" + +msgid "This {{enrollment}} doesn't have any {{notes}}" +msgstr "This {{enrollment}} doesn't have any {{notes}}" msgid "Error" msgstr "Error" @@ -1527,11 +1623,14 @@ msgstr "No polygon captured" msgid "Event completed" msgstr "Event completed" -msgid "Notes about this event" -msgstr "Notes about this event" +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 "Write a note about this event" -msgstr "Write a note about this event" +msgid "This {{event}} doesn't have any {{notes}}" +msgstr "This {{event}} doesn't have any {{notes}}" msgid "after" msgstr "after" @@ -1539,6 +1638,19 @@ msgstr "after" msgid "before" msgstr "before" +msgid "" +"There are {{count}} scheduled {{eventLabel}} in this program in " +"{{orgUnitName}} on this day." +msgid_plural "" +"There are {{count}} scheduled {{eventLabel}} in this program in " +"{{orgUnitName}} on this day." +msgstr[0] "" +"There are {{count}} scheduled {{eventLabel}} in this program in " +"{{orgUnitName}} on this day." +msgstr[1] "" +"There are {{count}} scheduled {{eventLabel}} in this program in " +"{{orgUnitName}} on this day." + msgid "Schedule date info" msgstr "Schedule date info" @@ -1553,39 +1665,29 @@ 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 "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}}" +msgid "Scheduling {{event}} in {{stageName}} for {{programName}} in {{orgUnitName}}" +msgstr "Scheduling {{event}} in {{stageName}} for {{programName}} in {{orgUnitName}}" -msgid "Scheduling an event in {{stageName}} for {{programName}}" -msgstr "Scheduling an event in {{stageName}} for {{programName}}" +msgid "Scheduling {{event}} in {{stageName}} for {{programName}}" +msgstr "Scheduling {{event}} in {{stageName}} for {{programName}}" msgid "Schedule info" msgstr "Schedule info" -msgid "Event notes" -msgstr "Event notes" +msgid "{{event}} notes" +msgstr "{{event}} notes" -msgid "Write a note about this scheduled event" -msgstr "Write a note about this scheduled event" +msgid "Write a note about this scheduled {{event}}" +msgstr "Write a note about this scheduled {{event}}" + +msgid "This {{event}} doesn't have any notes" +msgstr "This {{event}} doesn't have any notes" msgid "Program or stage is invalid" msgstr "Program or stage is invalid" @@ -1596,8 +1698,8 @@ msgstr "Feedback" msgid "Indicators" msgstr "Indicators" -msgid "Save note" -msgstr "Save note" +msgid "Save {{note}}" +msgstr "Save {{note}}" msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" @@ -1608,8 +1710,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 {{enrollment}} can be edited in the {{Enrollment}} " +"widget." +msgstr "" +"Information about this {{enrollment}} can be edited in the {{Enrollment}} " +"widget." msgid "{{trackedEntityName}} profile" msgstr "{{trackedEntityName}} profile" @@ -1641,11 +1747,11 @@ 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." +"{{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." +"{{enrollments}} and {{events}} in all programs." msgid "There was a problem deleting the {{trackedEntityTypeName}}" msgstr "There was a problem deleting the {{trackedEntityTypeName}}" @@ -1659,38 +1765,41 @@ 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 for {{trackedEntityTypeName}}" +msgstr "No {{attributes}} configured for {{trackedEntityTypeName}}" -msgid "No attributes configured" -msgstr "No attributes configured" +msgid "No {{attributes}} configured" +msgstr "No {{attributes}} configured" msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" -msgid "Choose a {{linkableStageLabel}} event" -msgstr "Choose a {{linkableStageLabel}} event" +msgid "Choose a {{linkableStageLabel}} {{event}}" +msgstr "Choose a {{linkableStageLabel}} {{event}}" + +msgid "Select {{event}}" +msgstr "Select {{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 }} 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 "{{ linkableStageLabel }} has no linkable events" -msgstr "{{ linkableStageLabel }} has no linkable events" +msgid "relationships" +msgstr "relationships" msgid "Actions - {{relationshipName}}" msgstr "Actions - {{relationshipName}}" -msgid "Ambiguous relationships, contact system administrator" -msgstr "Ambiguous relationships, contact system administrator" +msgid "Ambiguous {{relationships}}, contact system administrator" +msgstr "Ambiguous {{relationships}}, contact system administrator" msgid "Enter details" msgstr "Enter details" -msgid "Linked event" -msgstr "Linked event" +msgid "Linked {{event}}" +msgstr "Linked {{event}}" msgid "Enter details now" msgstr "Enter details now" @@ -1698,11 +1807,11 @@ 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 "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 "An error occurred while linking the {{event}}" +msgstr "An error occurred while linking the {{event}}" msgid "Scheduled date" msgstr "Scheduled date" @@ -1716,23 +1825,32 @@ 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 {{stage}} can only have one {{event}}" +msgstr "This {{stage}} can only have one {{event}}" -msgid "New {{ eventName }} event" -msgstr "New {{ eventName }} 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 "{{occurredAt}} belongs to an expired period. {{event}} cannot be deleted" +msgstr "{{occurredAt}} belongs to an expired period. {{event}} cannot be deleted" + +msgid "An error occurred while deleting the {{event}}" +msgstr "An error occurred while deleting the {{event}}" + +msgid "Delete {{event}}" +msgstr "Delete {{event}}" -msgid "An error occurred while deleting the event" -msgstr "An error occurred while deleting the event" +msgid "Deleting this {{event}} is permanent and cannot be undone." +msgstr "Deleting this {{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 "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 "Yes, delete {{event}}" +msgstr "Yes, delete {{event}}" + +msgid "An error occurred when updating {{event}} status" +msgstr "An error occurred when updating {{event}} status" msgid "Unskip" msgstr "Unskip" @@ -1740,8 +1858,11 @@ 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 "Events" +msgstr "Events" + +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" @@ -1749,13 +1870,13 @@ 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 "{{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 "{{ count }} {{eventLabel}}" +msgid_plural "{{ count }} {{eventLabel}}" +msgstr[0] "{{ count }} {{eventLabel}}" +msgstr[1] "{{ count }} {{eventLabel}}" msgid "{{ overdueEvents }} overdue" msgstr "{{ overdueEvents }} overdue" @@ -1763,61 +1884,67 @@ msgstr "{{ overdueEvents }} overdue" msgid "{{ scheduledEvents }} scheduled" msgstr "{{ scheduledEvents }} scheduled" -msgid "No stages found in this program" -msgstr "No stages found in this program" +msgid "stages" +msgstr "stages" + +msgid "No {{stages}} found in this program" +msgstr "No {{stages}} found in this program" + +msgid "Stages" +msgstr "Stages" -msgid "Stages and Events" -msgstr "Stages and Events" +msgid "{{stages}} and {{events}}" +msgstr "{{stages}} and {{events}}" -msgid "An error occurred while unlinking and deleting the event." -msgstr "An error occurred while unlinking and deleting the event." +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 "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 "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 " +"This action permanently removes the link, linked {{event}}, and all related " "data." msgstr "" -"This action permanently removes the link, linked event, and all related " +"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 "Yes, unlink and delete linked {{event}}" +msgstr "Yes, unlink and delete linked {{event}}" -msgid "Unlink event" -msgstr "Unlink 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 "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 "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 "Yes, unlink {{event}}" +msgstr "Yes, unlink {{event}}" -msgid "View linked event" -msgstr "View linked 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 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 "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" +"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" +"This {{stageName}} {{event}} is linked to a {{linkedStageName}} {{event}}. " +"Review the linked {{event}} details before entering data below" msgid "Scheduled" msgstr "Scheduled" @@ -1864,50 +1991,57 @@ msgstr "File" msgid "Image" msgstr "Image" -msgid "New {{trackedEntityTypeName}} relationship" -msgstr "New {{trackedEntityTypeName}} relationship" +msgid "New {{trackedEntityTypeName}} {{relationship}}" +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 "Go back without saving {{relationship}}" +msgstr "Go back without saving {{relationship}}" -msgid "New Relationship" -msgstr "New Relationship" +msgid "Relationship" +msgstr "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 "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 "" +"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 "{{trackedEntityTypeName}} {{relationships}}" +msgstr "{{trackedEntityTypeName}} {{relationships}}" -msgid "Delete relationship" -msgstr "Delete relationship" +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 "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 "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 "Yes, delete {{relationship}}" +msgstr "Yes, delete {{relationship}}" -msgid "An error occurred while deleting the relationship." -msgstr "An error occurred while deleting the 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 "This enrollment doesn't have any relationships" -msgstr "This enrollment doesn't have any relationships" +msgid "This {{enrollment}} doesn't have any {{relationships}}" +msgstr "This {{enrollment}} doesn't have any {{relationships}}" msgid "Type" msgstr "Type" @@ -1921,32 +2055,35 @@ msgstr "Program stage name" msgid "Working list could not be loaded" msgstr "Working list could not be loaded" +msgid "{{occurredAt}} belongs to an expired period. Event cannot be deleted" +msgstr "{{occurredAt}} belongs to an expired period. Event cannot be deleted" + msgid "Download data..." 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 "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 "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 "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 "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 "Error completing {{events}}" +msgstr "Error completing {{events}}" -msgid "There was an error completing the events." -msgstr "There was an error completing the events." +msgid "There was an error completing the {{events}}." +msgstr "There was an error completing the {{events}}." msgid "Details (Advanced)" msgstr "Details (Advanced)" @@ -1954,26 +2091,23 @@ 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 "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 "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 "An error occurred while deleting the {{events}}" +msgstr "An error occurred while deleting the {{events}}" -msgid "Delete events" -msgstr "Delete 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 "Owner organisation unit" -msgstr "Owner organisation unit" +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" @@ -1981,44 +2115,53 @@ msgstr "Registration Date" msgid "Inactive" msgstr "Inactive" -msgid "Enrollment status" -msgstr "Enrollment status" +msgid "Owner {{orgUnit}}" +msgstr "Owner {{orgUnit}}" + +msgid "{{event}} status" +msgstr "{{event}} status" + +msgid "{{event}} {{orgUnit}}" +msgstr "{{event}} {{orgUnit}}" msgid "Follow up" msgstr "Follow up" +msgid "{{enrollment}} status" +msgstr "{{enrollment}} status" + 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 {{enrollments}}" +msgstr "You do not have access to bulk complete {{enrollments}}" msgid "" -"Some enrollments were completed successfully, but there was an error while " -"completing the rest. Please see the details below." +"Some {{enrollments}} 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." +"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." +"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." +"There was an error while completing the {{enrollments}}. Please see the " +"details below." msgid "" -"An unexpected error occurred while fetching the enrollments. Please try " +"An unexpected error occurred while fetching the {{enrollments}}. Please try " "again." msgstr "" -"An unexpected error occurred while fetching the enrollments. Please try " +"An unexpected error occurred while fetching the {{enrollments}}. 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 {{enrollments}} in the selection." +msgstr "There are currently no active {{enrollments}} in the selection." -msgid "All enrollments are already completed or cancelled." -msgstr "All enrollments are already completed or cancelled." +msgid "All {{enrollments}} are already completed or cancelled." +msgstr "All {{enrollments}} 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." @@ -2030,70 +2173,74 @@ msgid_plural "{{count}} enrollment already marked as completed will not be chang 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 "Mark all events within enrollments as complete" -msgstr "Mark all events within enrollments as complete" +msgid "Mark all {{events}} within {{enrollments}} as complete" +msgstr "Mark all {{events}} within {{enrollments}} as complete" -msgid "Complete enrollments" -msgstr "Complete enrollments" +msgid "Complete {{enrollments}}" +msgstr "Complete {{enrollments}}" -msgid "Error completing enrollments" -msgstr "Error completing enrollments" +msgid "Error completing {{enrollments}}" +msgstr "Error completing {{enrollments}}" -msgid "No active enrollments to complete" -msgstr "No active enrollments to complete" +msgid "No active {{enrollments}} to complete" +msgstr "No active {{enrollments}} to complete" msgid "Complete {{count}} enrollment" msgid_plural "Complete {{count}} enrollment" msgstr[0] "Complete {{count}} enrollment" msgstr[1] "Complete {{count}} enrollments" -msgid "An error occurred when completing the enrollments" -msgstr "An error occurred when completing the enrollments" +msgid "An error occurred when completing the {{enrollments}}" +msgstr "An error occurred when completing the {{enrollments}}" -msgid "An unknown error occurred when completing enrollments" -msgstr "An unknown error occurred when completing enrollments" +msgid "An unknown error occurred when completing {{enrollments}}" +msgstr "An unknown error occurred when completing {{enrollments}}" -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 {{enrollments}}" +msgstr "You do not have access to delete {{enrollments}}" -msgid "Delete enrollments" -msgstr "Delete enrollments" +msgid "Delete {{enrollments}}" +msgstr "Delete {{enrollments}}" -msgid "Delete selected enrollments" -msgstr "Delete selected enrollments" +msgid "Delete selected {{enrollments}}" +msgstr "Delete selected {{enrollments}}" -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 {{enrollments}}. Please try " +"again." +msgstr "" +"An error occurred while loading the selected {{enrollments}}. 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 {{enrollments}}, 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 {{enrollments}}, 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 {{enrollments}} ({{count}})" +msgid_plural "Active {{enrollments}} ({{count}})" +msgstr[0] "Active {{enrollments}} ({{count}})" +msgstr[1] "Active {{enrollments}} ({{count}})" -msgid "Completed enrollments ({{count}})" -msgid_plural "Completed enrollments ({{count}})" -msgstr[0] "Completed enrollments ({{count}})" -msgstr[1] "Completed enrollments ({{count}})" +msgid "Completed {{enrollments}} ({{count}})" +msgid_plural "Completed {{enrollments}} ({{count}})" +msgstr[0] "Completed {{enrollments}} ({{count}})" +msgstr[1] "Completed {{enrollments}} ({{count}})" -msgid "Cancelled enrollments ({{count}})" -msgid_plural "Cancelled enrollments ({{count}})" -msgstr[0] "Cancelled enrollments ({{count}})" -msgstr[1] "Cancelled enrollments ({{count}})" +msgid "Cancelled {{enrollments}} ({{count}})" +msgid_plural "Cancelled {{enrollments}} ({{count}})" +msgstr[0] "Cancelled {{enrollments}} ({{count}})" +msgstr[1] "Cancelled {{enrollments}} ({{count}})" msgid "Delete {{count}} enrollment" msgid_plural "Delete {{count}} enrollment" msgstr[0] "Delete {{count}} enrollment" msgstr[1] "Delete {{count}} enrollments" -msgid "An error occurred when deleting enrollments" -msgstr "An error occurred when deleting enrollments" +msgid "An error occurred when deleting {{enrollments}}" +msgstr "An error occurred when deleting {{enrollments}}" msgid "Delete {{ trackedEntityName }} with all enrollments" msgstr "Delete {{ trackedEntityName }} with all enrollments" @@ -2219,6 +2366,9 @@ msgstr "Program not found" msgid "Program is not a tracker program" msgstr "Program is not a tracker program" +msgid "organisation unit could not be retrieved. Please try again later." +msgstr "organisation unit could not be retrieved. Please try again later." + msgid "Could not save event" msgstr "Could not save event" 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..c69c95c55b 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 { useProgramLabel } from '../../../../metaData'; type Props = { programId: string; @@ -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'); }; @@ -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) { @@ -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..0bdf031088 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 { 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'; @@ -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); @@ -101,14 +105,14 @@ 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)), @@ -116,14 +120,14 @@ const BreadcrumbsPlain = ({ { 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, }, @@ -135,6 +139,8 @@ const BreadcrumbsPlain = ({ onBackToMainPage, onBackToDashboard, onBackToViewEvent, + enrollment, + event, ]); 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..e01d8bb155 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 { useProgramLabel } from '../../../../metaData'; type Template = { id: string; @@ -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 ?? {}; @@ -66,6 +68,7 @@ export const useWorkingListLabel = ({ isSameProgram, selectedTemplate, selectedTemplateId, + DefaultFilterLabels, ]); 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..9e39113c89 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx @@ -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'; @@ -43,6 +44,7 @@ const EventBreadcrumbPlain = ({ }: Props) => { const [openWarning, setOpenWarning] = useState(null); const { label } = useWorkingListLabel({ programId }); + const event = useStageLabel('event', { programId }) ?? i18n.t('event'); 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: 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, }, @@ -88,6 +90,7 @@ const EventBreadcrumbPlain = ({ onBackToViewEvent, onBackToMainPage, page, + event, ]); 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..5d11f47365 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,7 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useSelector } from 'react-redux'; +import { useStageLabel } from '../../../../metaData'; type Template = { id: string; @@ -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...'); @@ -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, 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..b04a670191 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 { useProgramLabel } from '../../metaData'; type Props = { currentSearchScopeId?: string, @@ -78,6 +79,8 @@ const CardListButtons: FC = ({ 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> = useCallback((_, event) => { event.stopPropagation(); @@ -115,7 +118,7 @@ const CardListButtons: FC = ({ { 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, }, { 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..008de259ba 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx @@ -16,6 +16,7 @@ import { dataElementTypes, getTrackerProgramThrowIfNotFound, OptionSet, + useProgramLabel, type TrackerProgram, } from '../../metaData'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; @@ -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; } @@ -217,11 +220,11 @@ const CardListItemIndex = ({ return (<> 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..14e5b4a08f 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 { 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, @@ -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/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx index 06697c00e2..456a9b20ac 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 @@ -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'; @@ -18,7 +19,10 @@ const styles = (theme: any) => ({ const NewEventsListPlain = (props: Props & WithStyles) => { 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; } @@ -27,16 +31,15 @@ const NewEventsListPlain = (props: Props & WithStyles) => {
- {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, })}
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..2594e44ad0 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 @@ -5,6 +5,9 @@ import type { PlainProps, PlainPropsWithEvents } from './completeModal.types'; export const CompleteEnrollmentAndEventsModalComponent = ({ programStageName, + enrollmentLabel, + eventSingularLabel, + eventPluralLabel, programStagesWithActiveEvents, programStagesWithoutAccess, onCancel, @@ -19,19 +22,28 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ })} -

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

+

+ {i18n.t('Would you like to complete the {{enrollmentLabel}} and all active {{eventPluralLabel}} as well?', { + enrollmentLabel, + eventPluralLabel, + interpolation: { escapeValue: false }, + })} +

{Object.keys(programStagesWithActiveEvents).length !== 0 && ( <> - {i18n.t('The following events will be completed:')} + {i18n.t('The following {{eventPluralLabel}} will be completed:', { + eventPluralLabel, + interpolation: { escapeValue: false }, + })} {Object.keys(programStagesWithActiveEvents).map((key) => { const { count, name } = programStagesWithActiveEvents[key]; + const eventLabel = count === 1 ? eventSingularLabel : eventPluralLabel; return (
    - {i18n.t('{{count}} event in {{programStageName}}', { + {i18n.t('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel, programStageName: name, interpolation: { escapeValue: false }, })} @@ -43,16 +55,19 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ {Object.keys(programStagesWithoutAccess).length !== 0 && ( <> - {i18n.t('The following events will not be completed due to lack of access:')} + {i18n.t('The following {{eventPluralLabel}} will not be completed due to lack of access:', { + eventPluralLabel, + interpolation: { escapeValue: false }, + })} {Object.keys(programStagesWithoutAccess).map((key) => { const { count, name } = programStagesWithoutAccess[key]; + const eventLabel = count === 1 ? eventSingularLabel : eventPluralLabel; return (
      - {i18n.t('{{count}} event in {{programStageName}}', { + {i18n.t('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel, programStageName: name, interpolation: { escapeValue: false }, })} @@ -65,10 +80,17 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ ); 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 ea486c8df5..17ee407bbd 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 { useProgramLabel } from '../../../../metaData'; import { commitEnrollmentAndEvents, EnrollmentAccessProvider, @@ -52,6 +53,7 @@ export const EnrollmentPageDefault = () => { const { fromClientDate } = useTimeZoneConversion(); const { status: widgetEnrollmentStatus } = useSelector(({ widgetEnrollment }: any) => widgetEnrollment); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); + const enrollmentLabel = useProgramLabel('enrollment', { programId }) ?? i18n.t('enrollment'); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); const { onLinkedRecordClick } = useLinkedRecordClick(); const { @@ -226,8 +228,9 @@ 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 {{enrollment}} yet', { enrollment: enrollmentLabel })} + indicatorEmptyText={i18n.t( + 'No indicator output for this {{enrollment}} yet', { enrollment: enrollmentLabel })} /> ); 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..7304b8758a 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 @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n'; import { colors, spacers, IconAdd16, IconCalendar16 } from '@dhis2/ui'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import type { OutputEffect } from '@dhis2/rules-engine-javascript'; +import { useStageLabel } from '../../../../../metaData'; import { Widget } from '../../../../Widget'; import { QuickActionButton } from './QuickActionButton/QuickActionButton'; import { tabMode } from '../../../EnrollmentAddEvent/NewEventWorkspace/newEventWorkspace.constants'; @@ -29,12 +30,13 @@ const EnrollmentQuickActionsComponentPlain = ({ const [open, setOpen] = useState(true); const { navigate } = useNavigate(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); + const event = useStageLabel('event', { programId }) ?? i18n.t('event'); const { anyStageWriteAccess } = useEnrollmentAccessContext(); const stagesWithEventCount = useMemo(() => stages.map((stage) => { const mutatedStage = { ...stage }; mutatedStage.eventCount = (events - ?.filter(event => event.programStage === stage.id) + ?.filter(stageEvent => stageEvent.programStage === stage.id) ?.length ); return mutatedStage; @@ -79,7 +81,7 @@ const EnrollmentQuickActionsComponentPlain = ({ > } - label={i18n.t('New event')} + label={i18n.t('New {{event}}', { event })} onClickAction={() => onNavigationFromQuickActions(tabMode.REPORT)} dataTest={'quick-action-button-report'} disabled={noStageAvailable} @@ -87,7 +89,7 @@ const EnrollmentQuickActionsComponentPlain = ({ } - label={i18n.t('Schedule an event')} + label={i18n.t('Schedule {{event}}', { event })} onClickAction={() => onNavigationFromQuickActions(tabMode.SCHEDULE)} dataTest={'quick-action-button-schedule'} disabled={noStageAvailable} 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..496a04641b 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 @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { useDispatch, useSelector } from 'react-redux'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { useProgramLabel, useStageLabel } from '../../../metaData'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { useMissingCategoriesInProgramSelection } from '../../../hooks/useMissingCategoriesInProgramSelection'; import { scopeTypes } from '../../../metaData/helpers/constants'; @@ -151,14 +152,15 @@ const styles: Readonly = { }, }; -const EnrollmentSelectionMessage = ({ enrollmentId }: { enrollmentId?: string }) => ( +const EnrollmentSelectionMessage = ({ enrollmentId, enrollment }: { enrollmentId?: string, enrollment: string }) => ( {enrollmentId ? - i18n.t('Invalid enrollment id {{enrollmentId}}.', { + i18n.t('Invalid {{enrollment}} id {{enrollmentId}}.', { + enrollment, enrollmentId, interpolation: { escapeValue: false }, }) : - i18n.t('Choose an enrollment to view the dashboard.') + i18n.t('Choose this {{enrollment}} to view the dashboard.', { enrollment }) } ); @@ -167,6 +169,12 @@ type PlainProps = Record; type Props = PlainProps & WithStyles; +const useMissingMessageLabels = (programId?: string) => ({ + enrollment: useProgramLabel('enrollment', { programId }) ?? i18n.t('enrollment'), + enrollments: useProgramLabel('enrollment', { programId, plural: true }) ?? i18n.t('enrollments'), + event: useStageLabel('event', { programId }) ?? i18n.t('event'), +}); + const MissingMessagePlain = ({ classes, }: Props) => { @@ -180,6 +188,7 @@ const MissingMessagePlain = ({ const { resetTeiId } = useResetTeiId(); const { teiDisplayName, tetId } = useSelector(({ enrollmentPage }: any) => enrollmentPage); const { programId, teiId, enrollmentId } = useLocationQuery(); + const { enrollment, enrollments, event } = useMissingMessageLabels(programId); const { trackedEntityName: tetName } = useScopeInfo(tetId); const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId); @@ -188,8 +197,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 {{enrollments}} for {{teiDisplayName}}', { + enrollments, teiDisplayName, interpolation: { escapeValue: false }, })} } @@ -206,20 +215,22 @@ const MissingMessagePlain = ({ { missingStatus === missingStatuses.MISSING_ENROLLMENT_SELECTION && - + } { missingStatus === missingStatuses.MISSING_ENROLLMENT_SELECTION_ADD_NEW &&
      - {i18n.t('There are no active enrollments.')} + {i18n.t('There are no active {{enrollments}}.', { enrollments })}
      - {i18n.t('Add new enrollment for {{teiDisplayName}} in this program.', { teiDisplayName })} + {i18n.t( + 'Add new {{enrollment}} for {{teiDisplayName}} in this program.', + { enrollment, teiDisplayName })}
      @@ -271,8 +282,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}} {{enrollment}}. ', { + teiDisplayName, programName, tetName, enrollment, interpolation: { escapeValue: false }, })}
      - {i18n.t('{{programName}} is an event program and does not have enrollments.', { - programName, interpolation: { escapeValue: false }, + {i18n.t('{{programName}} is an event program and does not have {{enrollments}}.', { + programName, enrollments, interpolation: { escapeValue: false }, })}
      - {i18n.t('Create a new event in this program.')} + {i18n.t('Create a new {{event}} in this program.', { event })}
      diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/TopBar.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/TopBar.container.tsx index 3fe783d602..bbefcaf292 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/TopBar.container.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/TopBar.container.tsx @@ -1,5 +1,6 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { useProgramLabel } from '../../../metaData'; import { ScopeSelector, useSetProgramId, @@ -31,6 +32,7 @@ export const TopBar = ({ teiDisplayName, enrollmentsAsOptions, }: TopBarProps) => { + const enrollmentLabel = useProgramLabel('enrollment', { programId }) ?? i18n.t('Enrollment'); const { setProgramIdAndResetEnrollmentContext } = useSetProgramId(); const { setOrgUnitId } = useSetOrgUnitId(); const { setEnrollmentId } = useSetEnrollmentId(); @@ -73,7 +75,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/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx index 5d54cdf067..25a2d2174a 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,7 @@ import { EnrollmentAccessProvider } from '../../common/EnrollmentOverviewDomain' import { EnrollmentPageKeys, } from '../../common/EnrollmentOverviewDomain/EnrollmentPageLayout/DefaultEnrollmentLayout.constants'; -import { TrackerProgram } from '../../../../metaData'; +import { TrackerProgram, useStageLabel } from '../../../../metaData'; const styles: Readonly = ({ typography }: any) => ({ container: { @@ -57,6 +57,7 @@ const EnrollmentAddEventPagePain = ({ classes, ...passOnProps }: Props & WithStyles) => { + const event = useStageLabel('event', { stageId, programId: program?.id }) ?? i18n.t('event'); if (pageFailure) { return (
      @@ -91,8 +92,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={i18n.t('No feedback for this {{event}} yet', { event })} + indicatorEmptyText={i18n.t('No indicator output for this {{event}} yet', { event })} />
      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 64a9110b4b..d79f85777f 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,7 @@ 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, useStageLabel } from '../../../../metaData'; export const EnrollmentAddEventPageDefault = ({ pageLayout, @@ -35,6 +35,7 @@ export const EnrollmentAddEventPageDefault = ({ commonDataError, }: ContainerProps) => { const { programId, stageId, orgUnitId, teiId, enrollmentId } = useLocationQuery(); + const programStageLabel = useStageLabel('programStage', { programId, stageId }) ?? i18n.t('Program stage'); const { navigate } = useNavigate(); const dispatch = useDispatch(); @@ -145,7 +146,7 @@ export const EnrollmentAddEventPageDefault = ({ error title={i18n.t('An error has occurred')} > - {i18n.t('Program stage is invalid')} + {i18n.t('{{programStage}} is invalid', { programStage: 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 fd6f703bb5..e89fd79b3a 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,7 @@ 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, useStageLabel } from '../../../../metaData'; import { WidgetEnrollmentEventNew } from '../../../WidgetEnrollmentEventNew'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; @@ -48,6 +48,8 @@ const NewEventWorkspacePlain = ({ const [isWarningVisible, setWarningVisible] = useState(false); const tempMode = useRef(undefined); const { stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const event = useStageLabel('event', { programId, stageId }) ?? i18n.t('event'); + const programStageLabel = useStageLabel('programStage', { programId, stageId }) ?? i18n.t('Stage'); const onHandleSwitchTab = (newMode: string) => { if (dataEntryHasChanges) { @@ -69,7 +71,9 @@ const NewEventWorkspacePlain = ({ if (!stage) { return renderWidget( -
      {i18n.t('Stage not found')}
      , +
      + {i18n.t('{{programStage}} not found', { programStage: programStageLabel })} +
      , ); } @@ -78,7 +82,7 @@ const NewEventWorkspacePlain = ({ return renderWidget(
      , ); 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..d24517f618 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 { useStageLabel } from '../../../../../metaData'; import type { Props } from './widgetStageHeader.types'; -export const WidgetStageHeader = ({ stage }: Props) => ( -
      - {stage?.stageForm.name ?? i18n.t('New Event')} -
      -); +export const WidgetStageHeader = ({ stage }: Props) => { + const event = useStageLabel('event', { stageId: stage?.id }) ?? i18n.t('Event'); + return ( +
      + {stage?.stageForm.name ?? i18n.t('New {{event}}', { event })} +
      + ); +}; 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..4d7e829517 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,7 @@ 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 { useStageLabel } from '../../../../metaData'; const styles: Readonly = { container: { @@ -50,59 +51,63 @@ const ProgramStageSelectorComponentPlain = ({ onSelectProgramStage, onCancel, classes, -}: ProgramStageSelectorPlainProps) => ( -
      - {programStages.map((programStage) => { - const disableStage = +}: ProgramStageSelectorPlainProps) => { + const events = useStageLabel('event', { plural: true }) ?? i18n.t('events'); + return ( +
      + {programStages.map((programStage) => { + const disableStage = !programStage.dataAccess.write || (!programStage.repeatable && programStage.eventCount > 0) || programStage.hiddenProgramStage; - return ( -
      - - - -
      - ); - })} - -
      -); + + +
      + ); + })} + +
      + ); +}; export const ProgramStageSelectorComponent = withStyles(styles)(ProgramStageSelectorComponentPlain); 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..867a035980 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 @@ -10,10 +10,14 @@ import { useProgramFromIndexedDB } from '../../../../utils/cachedDataHooks/usePr import { useNavigate, useLocationQuery, buildUrlQueryString } from '../../../../utils/routing'; import { useCoreOrgUnit } from '../../../../metadataRetrieval/coreOrgUnit'; import { useTrackerProgram } from '../../../../hooks/useTrackerProgram'; +import { useStageLabel } from '../../../../metaData'; export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId }: Props) => { const { navigate } = useNavigate(); + const programStageLabel = useStageLabel('programStage', { programId }) ?? i18n.t('stage'); + const programStagesLabel = useStageLabel('programStage', { programId, plural: true }) ?? i18n.t('Program Stages'); + const event = useStageLabel('event', { programId }) ?? i18n.t('event'); const { tab } = useLocationQuery(); const { error: enrollmentsError, enrollment, attributeValues } = useCommonEnrollmentDomainData( teiId, @@ -51,7 +55,7 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId id: currentStage.id, dataAccess: currentStage.access.data, eventCount: (enrollment?.events - ?.filter((event: any) => event.programStage === currentStage.id) + ?.filter((stageEvent: any) => stageEvent.programStage === currentStage.id) ?.length ), displayName: currentStage.displayName, @@ -103,7 +107,9 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId <> {program ? - : i18n.t('Program Stages could not be loaded')} + : i18n.t('{{programStages}} could not be loaded', { programStages: 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 8aa3a1bd59..092008bcf3 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 @@ -2,6 +2,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { ScopeSelector, SingleLockedSelect, useReset } from '../../../ScopeSelector'; import { TopBarActions } from '../../../TopBarActions'; +import { useProgramLabel, useStageLabel } from '../../../../metaData'; import type { Props } from './topBar.types'; export const EnrollmentAddEventTopBar = ({ @@ -24,6 +25,8 @@ export const EnrollmentAddEventTopBar = ({ enrollmentsAsOptions, }: Props) => { const { reset } = useReset(); + const enrollmentLabel = useProgramLabel('enrollment', { programId }) ?? i18n.t('Enrollment'); + const programStageLabel = useStageLabel('programStage', { programId }) ?? i18n.t('Stage'); return ( onResetEnrollmentId()} options={enrollmentsAsOptions || []} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} isUserInteractionInProgress={userInteractionInProgress} /> {stageName && ( @@ -72,7 +75,7 @@ export const EnrollmentAddEventTopBar = ({ }, ]} selectedValue="alwaysPreselected" - title={i18n.t('Stage')} + title={programStageLabel} isUserInteractionInProgress={userInteractionInProgress} /> 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..d098906d0e 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,6 +1,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { dataEntryIds } from 'capture-core/constants'; +import { useStageLabel } from '../../../metaData'; import type { PlainProps } from './EnrollmentEditEventPage.types'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; @@ -62,80 +63,83 @@ export const EnrollmentEditEventPageComponent = ({ onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, userInteractionInProgress, -}: PlainProps) => ( - - - - - -); +}: PlainProps) => { + const event = useStageLabel('event', { programId: program?.id, stageId }) ?? i18n.t('event'); + 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 d5dd0a2dc1..34e900ffbf 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 { type ProgramStage, useProgramLabel, useStageLabel } from '../../../metaData'; import { pageStatuses } from './EnrollmentEditEventPage.constants'; import { ScopeSelector, @@ -47,6 +47,8 @@ export const TopBar = ({ isUserInteractionInProgress, }: Props) => { const { setOrgUnitId } = useSetOrgUnitId(); + const enrollmentLabel = useProgramLabel('enrollment', { programId: programId ?? undefined }) ?? i18n.t('Enrollment'); + const programStageLabel = useStageLabel('programStage', { programId: programId ?? undefined }) ?? i18n.t('Stage'); const { resetProgramIdAndEnrollmentContext } = useResetProgramId(); const { resetOrgUnitId } = useResetOrgUnitId(); @@ -87,7 +89,7 @@ export const TopBar = ({ onClear={() => resetEnrollmentId('enrollment', { programId: programId ?? undefined, teiId })} options={enrollmentsAsOptions} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} isUserInteractionInProgress={isUserInteractionInProgress} /> {programStage && ( 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..832a072105 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 @@ -1,6 +1,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { useProgramLabel } from '../../../../../metaData'; import { IncompleteSelectionsMessage } from '../../../../IncompleteSelectionsMessage'; const styles: Readonly = { @@ -11,15 +12,18 @@ 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 orgUnit = useProgramLabel('orgUnit') ?? i18n.t('organisation unit'); + return ( +
      + + {i18n.t('The category option is not valid for the selected {{orgUnit}}.', { orgUnit })} + {' '} + {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/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx index cf5e3f0644..8932bcf168 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,7 +3,7 @@ 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, useProgramLabel } from '../../../../../metaData'; import { useProgramInfo } from '../../../../../hooks/useProgramInfo'; const styles: Readonly = { @@ -51,6 +51,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ classes, }: Props) => { const { program, programType } = useProgramInfo(programId); + const orgUnit = useProgramLabel('orgUnit', { programId }) ?? i18n.t('organisation unit'); const isTracker = programType === programTypes.TRACKER_PROGRAM; const trackedEntityName = program instanceof TrackerProgram @@ -66,7 +67,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ >
      - {i18n.t('Please select an organisation unit')} + {i18n.t('Please select {{orgUnit}}', { orgUnit })}
      {showWorkingListLink && ( )} {showSearchLink && ( 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..a1b2e5906d 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 @@ -2,6 +2,7 @@ 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 { useStageLabel } from '../../../../../../../metaData'; import { type Props } from './eventWorkingListsInitHeader.types'; export const styles = () => ({ @@ -25,24 +26,27 @@ export const styles = () => ({ }); const EventWorkingListsInitHeaderPlain = - ({ children, classes: { container, headerContainer, listContainer, title } }: Props & WithStyles) => ( -
      -
      - ) => { + const events = useStageLabel('event', { plural: true }) ?? i18n.t('events'); + return ( +
      +
      - {i18n.t('Registered events')} - -
      -
      - {children} + + {i18n.t('Registered {{events}}', { events })} + +
      +
      + {children} +
      -
      - ); + ); + }; export const EventWorkingListsInitHeader = withStyles(styles)(EventWorkingListsInitHeaderPlain) as ComponentType; 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..2a2bde3f46 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 @@ -10,6 +10,7 @@ import { Button } from '@dhis2/ui'; import type { ContainerProps } from './NewPage.types'; import { withErrorMessageHandler, withLoadingIndicator } from '../../../HOC'; import { NEW_TEI_DATA_ENTRY_ID, newPageStatuses } from './NewPage.constants'; +import { useProgramLabel } from '../../../metaData'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { RegistrationDataEntry } from './RegistrationDataEntry'; import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; @@ -43,6 +44,7 @@ const NewPagePlain = ({ }: Props) => { const { scopeType } = useScopeInfo(currentScopeId); const [selectedScopeId, setScopeId] = useState(currentScopeId); + const orgUnit = useProgramLabel('orgUnit', { programId: currentScopeId }) ?? i18n.t('organisation unit'); useEffect(() => { setScopeId(currentScopeId); @@ -101,7 +103,7 @@ const NewPagePlain = ({ newPageStatus === newPageStatuses.WITHOUT_ORG_UNIT_SELECTED && <> - {i18n.t('Choose an organisation unit to start reporting')} + {i18n.t('Choose {{orgUnit}} to start reporting', { orgUnit })}
      } {supportsChangelog && ( @@ -169,7 +173,10 @@ const EventDetailsSectionPlain = (props: PlainProps & { classes: any }) => { - + {renderActionsContainer()}
      )} 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..a1ad74c320 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 @@ -70,27 +70,41 @@ class ViewEventNewRelationshipWrapperPlain extends React.Component className={this.props.classes.headerContainer} >
      - {i18n.t('New event relationship')} + {i18n.t('New {{event}} {{relationship}}', { + event: this.props.eventLabel, + relationship: this.props.relationshipLabel, + })}
      ); render() { - const { classes, onCancel, ...passOnProps } = this.props; + const { classes, onCancel, eventLabel, relationshipLabel, ...passOnProps } = this.props; return (
      - {i18n.t('Adding relationship to event.')} + + {i18n.t('Adding {{relationship}} to {{event}}.', { + relationship: relationshipLabel, + event: eventLabel, + })} + - {i18n.t('Go back to event without saving relationship')} + {i18n.t('Go back to {{event}} without saving {{relationship}}', { + event: eventLabel, + relationship: relationshipLabel, + })}
      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..a3613af5d3 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,5 @@ export type PlainProps = { onCancel: () => void; + eventLabel: string; + relationshipLabel: string; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx index 509b1cecf6..cc8defca54 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import i18n from '@dhis2/d2-i18n'; import { OrgUnitFetcher } from 'capture-core/components/OrgUnitFetcher'; import { useSelector } from 'react-redux'; import { ViewEvent } from './ViewEventComponent/ViewEvent.container'; @@ -6,6 +7,7 @@ import { ViewEventNewRelationshipWrapper } from './Relationship/ViewEventNewRela import { TopBar } from './TopBar.container'; import { inMemoryFileStore } from '../../DataEntry/file/inMemoryFileStore'; import { useLocationQuery } from '../../../utils/routing'; +import { useStageLabel } from '../../../metaData'; type Props = { isUserInteractionInProgress: boolean, @@ -21,6 +23,9 @@ export const ViewEventPageComponent = ({ isUserInteractionInProgress, eventDetai programId: state.currentSelections.programId, })); + const eventLabel = useStageLabel('event') ?? i18n.t('event'); + const relationshipLabel = useStageLabel('relationship') ?? i18n.t('relationship'); + return ( { showAddRelationship ? - : + : 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..90a95c9bdc 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 @@ -1,6 +1,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withTheme } from 'capture-core-utils/styles'; +import { useProgramLabel } from '../../../../../../../metaData'; import { DATA_ENTRY_ID } from '../../registerTei.const'; import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; @@ -21,10 +22,12 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('relationship'); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, - disabledMessage: i18n.t('Enter details now is not available when creating a relationship'), + disabledMessage: i18n.t( + 'Enter details now is not available when creating this {{relationship}}', { relationship }), }, [relatedStageActions.LINK_EXISTING_RESPONSE]: { hidden: true }, }; 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..eb1ad7f721 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 @@ -1,6 +1,7 @@ import React, { type ComponentType, useCallback, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { useProgramLabel } from '../../../../../metaData'; import { UnsupportedAttributesNotification } from '../../../../../utils/warnings'; import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; import { TeiSearchResults } from './TeiSearchResults/TeiSearchResults.container'; @@ -26,6 +27,8 @@ const getStyles = (theme: any) => ({ const TeiSearchPlain = (props: Props & WithStyles) => { const [programSectionOpen, setProgramSectionOpen] = useState(true); + const attributes = useProgramLabel('attribute', { programId: props.selectedProgramId || undefined, plural: true }) + ?? i18n.t('attributes'); const getFormId = useCallback((searchGroupId) => { const contextId = props.selectedProgramId || props.selectedTrackedEntityTypeId || ''; @@ -99,7 +102,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'); + i18n.t('Search by {{attributes}}', { attributes }); const collapsed = props.openSearchGroupSection !== searchGroupId; const unsupportedAttributes = sg.unsupportedAttributes; return ( 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..b86fdc61f2 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 @@ -1,5 +1,6 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { useProgramLabel, useStageLabel } from '../../../../metaData'; import { pageStatuses } from '../../EnrollmentEditEvent/EnrollmentEditEventPage.constants'; import { IncompleteSelectionsMessage } from '../../../IncompleteSelectionsMessage'; import { WidgetEventEdit } from '../../../WidgetEventEdit'; @@ -23,17 +24,21 @@ export const WidgetEventEditWrapper = ({ pageStatus, ...passOnProps }: WidgetPro isError, } = useMetadataForProgramStage({ programId, stageId }); + const orgUnit = useProgramLabel('orgUnit', { programId }) ?? i18n.t('organisation unit'); + const enrollment = useProgramLabel('enrollment', { programId }) ?? i18n.t('enrollment'); + const event = useStageLabel('event', { programId, stageId }) ?? i18n.t('event'); + if (pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED) { return ( - {i18n.t('Choose an organisation unit to start reporting')} + {i18n.t('Choose {{orgUnit}} to start reporting', { orgUnit })} ); } if (pageStatus === pageStatuses.MISSING_DATA) { return ( - {i18n.t('The enrollment event data could not be found')} + {i18n.t('The {{enrollment}} {{event}} data could not be found', { enrollment, event })} ); } diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index bad84d8dfc..e22fb06a7f 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -3,6 +3,7 @@ import { IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; +import { useStageLabel, useTrackedEntityTypeLabel } from '../../metaData'; const styles = { label: { @@ -27,21 +28,36 @@ type Access = { programStage: boolean; }; -const getEnrollmentMessage = (): string => i18n.t('You only have view access to this enrollment'); +const getEnrollmentMessage = (enrollment: string): string => + i18n.t('You only have view access to this {{enrollment}}', { enrollment, escapeValue: false }); const getProgramMessage = (): string => i18n.t('You only have view access to this program'); -const getTrackedEntityMessage = (trackedEntityName: string | undefined): string => (trackedEntityName +const getTrackedEntityMessage = ( + trackedEntityName: string | undefined, + trackedEntityType: string, +): string => (trackedEntityName ? 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')); + : i18n.t('You only have view access to this {{trackedEntityType}}', { trackedEntityType, escapeValue: false })); + +const getProgramStageMessage = (multipleStages: boolean, programStage: string, programStages: string): string => + (multipleStages + ? i18n.t('You only have view access to these {{programStages}}', { programStages, escapeValue: false }) + : i18n.t('You only have view access to this {{programStage}}', { programStage, escapeValue: false })); -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 getExpiredPeriodMessage = (event: string): string => + i18n.t('This {{event}} is outside the valid editing period', { event, escapeValue: false }); -const getExpiredPeriodMessage = (): string => i18n.t('This event is outside the valid editing period'); +const getCompletedEventMessage = (event: string): string => + i18n.t('This {{event}} has been completed', { event, escapeValue: false }); -const getCompletedEventMessage = (): string => i18n.t('This event has been completed'); +type Labels = { + enrollment: string; + trackedEntityType: string; + programStage: string; + programStages: string; + event: string; +}; const getReadOnlyMessage = ( access: Access, @@ -49,13 +65,16 @@ const getReadOnlyMessage = ( multipleStages: boolean, eventWithinValidPeriod: boolean, canEditCompletedEvent: boolean, + labels: Labels, ): string => { - if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(); + if (!access.program && !access.trackedEntityType && !access.programStage) { + return getEnrollmentMessage(labels.enrollment); + } if (!access.program) return getProgramMessage(); - if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); - if (!access.programStage) return getProgramStageMessage(multipleStages); - if (!eventWithinValidPeriod) return getExpiredPeriodMessage(); - if (!canEditCompletedEvent) return getCompletedEventMessage(); + if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName, labels.trackedEntityType); + if (!access.programStage) return getProgramStageMessage(multipleStages, labels.programStage, labels.programStages); + if (!eventWithinValidPeriod) return getExpiredPeriodMessage(labels.event); + if (!canEditCompletedEvent) return getCompletedEventMessage(labels.event); return ''; }; @@ -75,12 +94,20 @@ const ReadOnlyBadgePlain = ({ trackedEntityType: trackedEntityTypeWriteAccess, programStage: programStageWriteAccess, }; + const labels: Labels = { + enrollment: useStageLabel('enrollment') ?? i18n.t('enrollment'), + trackedEntityType: useTrackedEntityTypeLabel('trackedEntityType') ?? i18n.t('tracked entity type'), + programStage: useStageLabel('programStage') ?? i18n.t('program stage'), + programStages: useStageLabel('programStage', { plural: true }) ?? i18n.t('program stages'), + event: useStageLabel('event') ?? i18n.t('event'), + }; const message = getReadOnlyMessage( access, trackedEntityName, multipleStages, eventWithinValidPeriod, canEditCompletedEvent, + labels, ); if (!message) return null; diff --git a/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramList.tsx b/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramList.tsx index bf71e46e05..24e06d9e62 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramList.tsx +++ b/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/Program/ProgramList.tsx @@ -3,7 +3,7 @@ import { MenuDivider, MenuItem, Button, colors, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; import { FiltrableMenuItems } from '../FiltrableMenuItems'; -import type { Program, Icon } from '../../../../metaData'; +import { useProgramLabel, type Program, type Icon } from '../../../../metaData'; import { OptionLabel } from '../../OptionLabel'; const styles = () => ({ @@ -33,6 +33,7 @@ type Props = OwnProps & WithStyles; const ProgramListPlain = ({ programOptions, programsArray, onChange, onResetOrgUnit, classes }: Props) => { const areAllProgramsAvailable = programOptions.length === programsArray.filter(program => program.access.data.read).length; + const orgUnit = useProgramLabel('orgUnit') ?? i18n.t('organisation unit'); return ( <> @@ -59,7 +60,7 @@ const ProgramListPlain = ({ programOptions, programsArray, onChange, onResetOrgU <>
      - {i18n.t('Some programs are being filtered by the chosen organisation unit')} + {i18n.t('Some programs are being filtered by the chosen {{orgUnit}}', { orgUnit })} 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..6ffa61f1fa 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,7 @@ import { searchScopes } from './SearchBox.constants'; import { useScopeTitleText, useScopeInfo } from '../../hooks'; import { useSearchOption } from './hooks'; import { SearchStatus } from './SearchStatus'; -import { scopeTypes } from '../../metaData'; +import { scopeTypes, useTrackedEntityTypeLabel } from '../../metaData'; const getStyles: Readonly = { half: { @@ -91,6 +91,7 @@ function renderFooterContent(args: { searchGroupsForSelectedScope: SearchGroups; availableSearchOption?: AvailableSearchOption; trackedEntityName: string; + trackedEntityTypeLabel: string; }) { if (args.isLoading) { return ; @@ -120,7 +121,7 @@ function renderFooterContent(args: { })} > {/* eslint-disable-next-line max-len */} - {i18n.t('Try selecting a different tracked entity type, or try searching in a program by choosing one from the top bar.')} + {i18n.t('Try selecting a different {{trackedEntityType}}, or try searching in a program by choosing one from the top bar.', { trackedEntityType: args.trackedEntityTypeLabel })} , ); } @@ -145,6 +146,7 @@ const Index = ({ ); const { trackedEntityName } = useScopeInfo(selectedSearchScopeId ?? null); const titleText = useScopeTitleText(selectedSearchScopeId ?? null); + const trackedEntityTypeLabel = useTrackedEntityTypeLabel('trackedEntityType') ?? i18n.t('tracked entity type'); const { searchOption: availableSearchOption, isLoading, @@ -212,6 +214,7 @@ const Index = ({ searchGroupsForSelectedScope, availableSearchOption, trackedEntityName, + trackedEntityTypeLabel, })} ); 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 847bdbb069..53797fbe19 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,7 @@ 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 { useProgramLabel } from '../../../metaData'; const styles: Readonly = (theme: any) => ({ searchDomainsContainer: { @@ -102,6 +103,7 @@ const SearchFormIndex = ({ keptFallbackSearchFormValues, }: Props & WithStyles) => { const { resultsPageSize } = useContext(ResultsPageSizeContext) as any; + const attributes = useProgramLabel('attribute', { plural: true }) ?? i18n.t('attributes'); useFormDataLifecycle( searchGroupsForSelectedScope, @@ -287,7 +289,7 @@ const SearchFormIndex = ({ minAttributesRequiredToSearch, unsupportedAttributes, }) => { - const searchByText = i18n.t('Search by attributes'); + const searchByText = i18n.t('Search by {{attributes}}', { attributes }); const isSearchSectionCollapsed = !(expandedFormId === formId); return (
      @@ -373,6 +375,7 @@ const SearchFormIndex = ({ resultsPageSize, error, expandedFormId, + attributes, ]); }; 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..92aad28962 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,7 @@ 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, useProgramLabel } from '../../../metaData'; const SearchPagination = withNavigation()(Pagination); @@ -124,6 +124,8 @@ const SearchResultsIndex = ({ const currentProgramId = (currentSearchScopeType === searchScopes.PROGRAM) ? currentSearchScopeId : ''; const { trackedEntityName } = useScopeInfo(currentSearchScopeId); + const attributes = useProgramLabel('attribute', { plural: true, programId: currentProgramId || undefined }) + ?? i18n.t('attributes'); return (<>
      ); 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..1aa2f5ae40 100644 --- a/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx +++ b/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx @@ -2,7 +2,7 @@ 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, useStageLabel } from '../../metaData'; import { useScopeInfo } from '../../hooks/useScopeInfo'; import type { PlainProps } from './TopBarActions.types'; @@ -27,6 +27,7 @@ const ActionButtonsPlain = ({ }: PlainProps & WithStyles) => { const { trackedEntityName, scopeType, programName } = useScopeInfo(selectedProgramId); const [openSearch, setOpenSearch] = useState(false); + const event = useStageLabel('event', { programId: selectedProgramId ?? undefined }) ?? i18n.t('event'); useEffect(() => { setOpenSearch(false); @@ -65,7 +66,7 @@ const ActionButtonsPlain = ({ trackedEntityType: trackedEntityName, interpolation: { escapeValue: false }, }) - : i18n.t('Create new event') + : i18n.t('Create new {{event}}', { event }) } )} diff --git a/src/core_modules/capture-core/components/TrackedEntityTypeSelector/TrackedEntityTypeSelector.component.tsx b/src/core_modules/capture-core/components/TrackedEntityTypeSelector/TrackedEntityTypeSelector.component.tsx index 85bac0294b..951dc267e8 100644 --- a/src/core_modules/capture-core/components/TrackedEntityTypeSelector/TrackedEntityTypeSelector.component.tsx +++ b/src/core_modules/capture-core/components/TrackedEntityTypeSelector/TrackedEntityTypeSelector.component.tsx @@ -3,7 +3,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { spacers, SimpleSingleSelectField } from '@dhis2/ui'; import type { Props, SelectOption } from './TrackedEntityTypeSelector.types'; -import { scopeTypes } from '../../metaData'; +import { scopeTypes, useTrackedEntityTypeLabel } from '../../metaData'; import { useTrackedEntityTypesWithCorrelatedPrograms, useCurrentTrackedEntityTypeId } from '../../hooks'; const styles: Readonly = ({ typography }: any) => ({ @@ -21,6 +21,9 @@ export const TrackedEntityTypeSelectorPlain = const trackedEntityTypesWithCorrelatedPrograms = useTrackedEntityTypesWithCorrelatedPrograms(); const selectedSearchScopeId = useCurrentTrackedEntityTypeId(); const [filterValue, setFilterValue] = useState(''); + const trackedEntityType = useTrackedEntityTypeLabel('trackedEntityType') ?? i18n.t('tracked entity type'); + const trackedEntityTypes = useTrackedEntityTypeLabel('trackedEntityType', { plural: true }) + ?? i18n.t('tracked entity types'); const options: SelectOption[] = useMemo(() => Object.values(trackedEntityTypesWithCorrelatedPrograms) @@ -67,13 +70,13 @@ export const TrackedEntityTypeSelectorPlain = helpText={footerText} options={filteredOptions} value={selectedOption?.value} - placeholder={i18n.t('Select tracked entity type')} + placeholder={i18n.t('Select {{trackedEntityType}}', { trackedEntityType })} filterable - filterPlaceholder={i18n.t('Type to filter tracked entity types')} + filterPlaceholder={i18n.t('Type to filter {{trackedEntityTypes}}', { trackedEntityTypes })} filterValue={filterValue} onFilterChange={setFilterValue} onChange={handleSelectionChange} - empty={i18n.t('No tracked entity types available')} + empty={i18n.t('No {{trackedEntityTypes}} available', { trackedEntityTypes })} />
      ); 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..9bf425a119 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx +++ b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx @@ -2,6 +2,7 @@ import React from 'react'; 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 { useStageLabel } from '../../metaData'; import type { Assignee } from './WidgetAssignee.types'; const styles = () => ({ @@ -34,8 +35,10 @@ type Props = { avatarId?: string; } & WithStyles; -const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, classes }: Props) => ( - assignee ? ( +const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, classes }: Props) => { + const event = useStageLabel('event') ?? i18n.t('event'); + + return assignee ? (
      {i18n.t('Assigned to')} @@ -57,7 +60,10 @@ const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, classe ) : (
      - {i18n.t('No one is assigned to this event')} + {i18n.t('No one is assigned to this {{event}}', { + event, + interpolation: { escapeValue: false }, + })}
      {!readOnly && (
      - ) -); + ); +}; export const DisplayMode = withStyles(styles)(DisplayModePlain); 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..5aa0a2de96 100644 --- a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx +++ b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx @@ -9,6 +9,7 @@ import { import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { PlainProps } from './WidgetBreakingTheGlass.types'; +import { useProgramLabel } from '../../metaData'; import { Widget } from '../Widget'; const styles: Readonly = ({ typography }: any) => ({ @@ -34,6 +35,7 @@ const WidgetBreakingTheGlassPlain = ({ onCancel, classes, }: Props) => { + const enrollments = useProgramLabel('enrollment', { plural: true }) ?? i18n.t('enrollments'); const [reason, setReason] = useState(''); const reasonChangeHandler = useCallback(({ value }: any) => { setReason(value); @@ -49,11 +51,17 @@ const WidgetBreakingTheGlassPlain = ({ >
      - {i18n.t('Check for enrollments')} + {i18n.t('Check for {{enrollments}}', { + enrollments, + interpolation: { escapeValue: false }, + })}

      - {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 {{enrollments}} in this protected program.', { + enrollments, + interpolation: { escapeValue: false }, + })} {' '} {i18n.t('All activity will be logged.')} @@ -70,7 +78,10 @@ const WidgetBreakingTheGlassPlain = ({
      - - - - - - -); + return ( +
        +
      • + {i18n.t('{{count}} event in {{programStageName}}', { + count, + defaultValue: '{{count}} event in {{programStageName}}', + defaultValue_plural: '{{count}} events in {{programStageName}}', + programStageName: name, + interpolation: { escapeValue: false }, + })} +
      • +
      + ); + })} + + )} + + + + + + + + + + + ); +}; 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..6b8c222a0e 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 @@ -11,12 +11,17 @@ import { } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import type { Props } from './delete.types'; +import { useProgramLabel } from '../../../../metaData'; import { ConditionalTooltip } from '../../../Tooltips/ConditionalTooltip/'; 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 = useProgramLabel('enrollment') ?? i18n.t('enrollment'); + const tooltipContent = i18n.t('You do not have access to delete this {{enrollment}}', { + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + }); return ( @@ -37,11 +42,20 @@ export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Pro onClose={() => setToggle(false)} dataTest="widget-enrollment-actions-modal" > - {i18n.t('Delete enrollment')} + {i18n.t('Delete {{enrollment}}', { + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + })} - {i18n.t('Are you sure you want to delete this enrollment?')} + {i18n.t('Are you sure you want to delete this {{enrollment}}?', { + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + })} {' '} - {i18n.t('This will permanently remove the current enrollment.')} + {i18n.t('This will permanently remove the current {{enrollment}}.', { + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + })} @@ -52,7 +66,10 @@ export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Pro destructive onClick={() => onDelete(enrollment)} > - {i18n.t('Yes, delete enrollment.')} + {i18n.t('Yes, delete {{enrollment}}.', { + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + })} 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..19f8f8387c 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,13 @@ import React from 'react'; import { IconFlag16, MenuItem } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { useProgramLabel } from '../../../../metaData'; import type { Props } from './followup.types'; -export const Followup = ({ enrollment, onUpdate }: Props) => - (enrollment.followUp ? ( +export const Followup = ({ enrollment, onUpdate }: Props) => { + const followUp = useProgramLabel('followUp') ?? i18n.t('follow-up'); + + return enrollment.followUp ? ( }) } icon={} - label={i18n.t('Remove mark for follow-up')} + label={i18n.t('Remove mark for {{followUp}}', { + followUp, + interpolation: { escapeValue: false }, + })} suffix="" /> ) : ( @@ -29,7 +35,11 @@ export const Followup = ({ enrollment, onUpdate }: Props) => }) } icon={} - label={i18n.t('Mark for follow-up')} + label={i18n.t('Mark for {{followUp}}', { + followUp, + interpolation: { escapeValue: false }, + })} suffix="" /> - )); + ); +}; 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..2b0138f0ba 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 @@ -4,6 +4,7 @@ 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 { useProgramLabel } from '../../../../metaData'; 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 enrollment = useProgramLabel('enrollment') ?? i18n.t('enrollment'); const showWarning = [ProgramAccessLevels.PROTECTED, ProgramAccessLevels.CLOSED].includes(programAccessLevel as any) && orgUnitScopes.destination === OrgUnitScopes.SEARCH; @@ -57,10 +59,12 @@ const InfoBoxesPlain = ({ {newOrgUnitName && (
      - {i18n.t('Transferring enrollment ownership from {{ownerOrgUnit}} to {{newOrgUnit}}{{escape}}', { + {i18n.t('Transferring {{enrollment}} ownership from {{ownerOrgUnit}} to {{newOrgUnit}}{{escape}}', { + enrollment, ownerOrgUnit: ownerOrgUnitName, newOrgUnit: newOrgUnitName, escape: '.', + interpolation: { escapeValue: false }, })}
      )} @@ -68,9 +72,13 @@ const InfoBoxesPlain = ({ {showWarning && (
      - {i18n.t('You will lose access to the enrollment when transferring ownership to {{organisationUnit}}.', { - organisationUnit: newOrgUnitName, - })} + {i18n.t( + 'You will lose access to the {{enrollment}} when transferring ownership to {{organisationUnit}}.', + { + enrollment, + organisationUnit: newOrgUnitName, + interpolation: { escapeValue: false }, + })}
      )}
      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..63aca2ec5f 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 { useProgramLabel } from '../../../metaData'; export const TransferModal = ({ enrollment, @@ -20,6 +21,8 @@ export const TransferModal = ({ onUpdateOwnership, isTransferLoading, }: TransferModalProps) => { + const orgUnit = useProgramLabel('orgUnit') ?? i18n.t('organisation unit'); + const enrollmentLabel = useProgramLabel('enrollment') ?? i18n.t('enrollment'); const { selectedOrgUnit, handleOrgUnitChange, @@ -51,7 +54,11 @@ export const TransferModal = ({
      - {i18n.t('Choose the organisation unit to which enrollment ownership should be transferred.')} + {i18n.t('Choose the {{orgUnit}} to which {{enrollment}} ownership should be transferred.', { + orgUnit, + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + })}
      const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); +const useEnrollmentWidgetLabels = () => ({ + enrollmentLabel: useProgramLabel('enrollment') ?? i18n.t('Enrollment'), + followUpLabel: useProgramLabel('followUp') ?? i18n.t('Follow-up'), +}); + const WidgetEnrollmentPlain = ({ classes, events, @@ -82,6 +87,7 @@ const WidgetEnrollmentPlain = ({ onAccessLostFromTransfer, }: PlainProps & WithStyles) => { const { programWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); + const { enrollmentLabel, followUpLabel } = useEnrollmentWidgetLabels(); const enrollmentReadOnly = readOnlyMode || !programWriteAccess; const [open, setOpenStatus] = useState(true); const { fromServerDate } = useTimeZoneConversion(); @@ -100,7 +106,7 @@ const WidgetEnrollmentPlain = ({ - {i18n.t('Enrollment')} + {enrollmentLabel} {showWidgetBadge && (
      {initError && (
      - {i18n.t('Enrollment widget could not be loaded. Please try again later')} + {i18n.t('{{enrollment}} widget could not be loaded. Please try again later', { + enrollment: enrollmentLabel, + interpolation: { escapeValue: false }, + })}
      )} {loading && } @@ -126,7 +135,7 @@ const WidgetEnrollmentPlain = ({
      {enrollment.followUp && ( - {i18n.t('Follow-up')} + {followUpLabel} )} 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..0296e00347 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 @@ -2,6 +2,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit'; import { Validated } from '../Validated/Validated.container'; +import { useProgramLabel } from '../../../metaData'; import type { OrgUnitFetcherProps } from './orgUnitFetcher.types'; export const OrgUnitFetcher = ({ @@ -9,11 +10,15 @@ export const OrgUnitFetcher = ({ ...passOnProps }: OrgUnitFetcherProps) => { const { error, orgUnit } = useCoreOrgUnit(orgUnitId); + const orgUnitLabel = useProgramLabel('orgUnit') ?? i18n.t('organisation unit'); if (error) { return (
      - {i18n.t('organisation unit could not be retrieved. Please try again later.')} + {i18n.t('{{orgUnit}} could not be retrieved. Please try again later.', { + orgUnit: orgUnitLabel, + interpolation: { escapeValue: false }, + })}
      ); } 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 bc1d4e6f1d..8b61d6dd51 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -6,10 +6,14 @@ import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { useLocationQuery } from '../../utils/routing'; +import { useProgramLabel } from '../../metaData'; export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); const { enrollmentId } = useLocationQuery(); + const enrollment = useProgramLabel('enrollment') ?? i18n.t('enrollment'); + const notesTitle = useProgramLabel('note', { plural: true }) ?? i18n.t('Notes'); + const notesLower = useProgramLabel('note', { plural: true }) ?? i18n.t('notes'); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); const { @@ -25,9 +29,20 @@ export const WidgetEnrollmentNote = () => { return (
      dispatch(startShowEditEventDataEntry(orgUnit, programCategory))} data-test="widget-enrollment-event-edit-button" > - {i18n.t('Edit event')} + {i18n.t('Edit {{event}}', { + event, + interpolation: { escapeValue: false }, + })} )} 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..ca44456043 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -6,9 +6,13 @@ import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useProgramLabel, useStageLabel } from '../../metaData'; export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); + const event = useStageLabel('event') ?? i18n.t('event'); + const notesTitle = useProgramLabel('note', { plural: true }) ?? i18n.t('Notes'); + const notesLower = useProgramLabel('note', { plural: true }) ?? i18n.t('notes'); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); const { @@ -24,9 +28,20 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { return (
      { + const eventSingular = useStageLabel('event') ?? i18n.t('event'); + const eventPlural = useStageLabel('event', { plural: true }) ?? i18n.t('events'); + if (!scheduleDate || !suggestedScheduleDate) { return null; } @@ -34,6 +38,14 @@ const InfoBoxPlain = ({ const absoluteDifference = Math.abs(dayDifference); const position = dayDifference > 0 ? i18n.t('after') : i18n.t('before'); const scheduledDateMatchesSuggested = scheduleDate === suggestedScheduleDate; + const orgUnitScheduleText = i18n.t( + 'There are {{count}} scheduled {{eventLabel}} in this program in {{orgUnitName}} on this day.', + { + count: eventCountInOrgUnit, + eventLabel: eventCountInOrgUnit === 1 ? eventSingular : eventPlural, + orgUnitName, + interpolation: { escapeValue: false }, + }); return ( @@ -58,17 +70,7 @@ 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, - }, - })} + {orgUnitScheduleText} )} 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..e29772f322 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,7 @@ import React, { useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { isValidOrgUnit } from 'capture-core-utils/validators/form'; +import { useProgramLabel } from '../../../metaData'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; import { @@ -45,6 +46,7 @@ export const ScheduleOrgUnit = ({ orgUnit, }: Props) => { const [touched, setTouched] = useState(false); + const orgUnitLabel = useProgramLabel('orgUnit') ?? i18n.t('Organisation unit'); const handleSelect = (event: any) => { setTouched(true); @@ -61,7 +63,7 @@ export const ScheduleOrgUnit = ({ return ( ( - - - {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 event = useStageLabel('event') ?? i18n.t('event'); + return ( + + + {orgUnitName + ? i18n.t('Scheduling {{event}} in {{stageName}} for {{programName}} in {{orgUnitName}}', + { event, orgUnitName, stageName, programName, interpolation: { escapeValue: false } }) + : i18n.t('Scheduling {{event}} in {{stageName}} for {{programName}}', + { event, stageName, programName, interpolation: { escapeValue: false } })} + + + ); +}; 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 38efaab9b0..78aae510dc 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,7 @@ import type { Props } from './widgetEventSchedule.types'; import { CategoryOptions } from './CategoryOptions/CategoryOptions.component'; import { Assignee } from './Assignee'; import { ScheduleOrgUnit } from './ScheduleOrgUnit/ScheduleOrgUnit.component'; +import { useStageLabel } from '../../metaData'; const styles = (theme: any) => ({ wrapper: { @@ -58,6 +59,9 @@ const WidgetEventSchedulePlain = ({ setValidation, ...passOnProps }: Props & WithStyles) => { + const event = useStageLabel('event', { stageId, programId }) ?? i18n.t('event'); + const eventCapitalized = useStageLabel('event', { stageId, programId }) ?? i18n.t('Event'); + const onSelectOrgUnit = (e: { id: string; displayName: string; path: string }) => { setScheduledOrgUnit({ id: e.id, @@ -122,12 +126,21 @@ const WidgetEventSchedulePlain = ({ } 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..10ea10f355 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { TextField } from '../../FormFields/New'; import { convertClientToList } from '../../../converters'; -import { dataElementTypes } from '../../../metaData'; +import { dataElementTypes, useProgramLabel } from '../../../metaData'; import type { OwnProps, NoteType } from './NoteSection.types'; const FocusTextField = withFocusSaver()(TextField); @@ -76,6 +76,7 @@ const NoteSectionPlain = ({ const [isEditing, setEditing] = useState(false); const [newNoteValue, setNewNoteValue] = useState(''); const { fromServerDate } = useTimeZoneConversion(); + const note = useProgramLabel('note') ?? i18n.t('note'); const handleChange = useCallback((value: string) => { setEditing(true); @@ -129,7 +130,7 @@ const NoteSectionPlain = ({
      {notes .sort((a, b) => moment(a.storedAt).valueOf() - moment(b.storedAt).valueOf()) - .map(note => ) + .map(noteItem => ) } {notes.length === 0 &&
      @@ -158,7 +159,10 @@ const NoteSectionPlain = ({ primary small > - {i18n.t('Save note')} + {i18n.t('Save {{note}}', { + note, + interpolation: { escapeValue: false }, + })} - {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 {{enrollment}} can be edited in the {{Enrollment}} widget.', { + enrollment: enrollmentLower, + Enrollment: enrollmentTitle, + interpolation: { escapeValue: false }, + })} + + + + ); +}; 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 74c4ff032d..c78d6effd4 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 @@ -3,8 +3,11 @@ import i18n from '@dhis2/d2-i18n'; import { Modal, ModalContent, ModalTitle, ModalActions, ButtonStrip, Button, NoticeBox } from '@dhis2/ui'; import type { Props } from './DeleteModal.types'; import { useDeleteTrackedEntity } from './hooks'; +import { useProgramLabel, useStageLabel } from '../../../../../metaData'; export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, setOpenModal, onDeleteSuccess }: Props) => { + const enrollments = useProgramLabel('enrollment', { plural: true }) ?? i18n.t('enrollments'); + const events = useStageLabel('event', { plural: true }) ?? i18n.t('events'); const [errorReports, setErrorReports] = useState>([]); const handleErrors = (errors: Array<{ message: string; uid: string }>) => { setErrorReports(errors); @@ -22,9 +25,11 @@ 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.', + {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.', { trackedEntityTypeName, + enrollments, + events, interpolation: { escapeValue: false }, }, )} 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 e4b5cc05df..a7d2b47274 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -7,6 +7,7 @@ import log from 'loglevel'; import { FlatList } from 'capture-ui'; import { useQueryClient } from '@tanstack/react-query'; import { errorCreator, FEATURES, useFeature } from 'capture-core-utils'; +import { useProgramLabel } from '../../metaData'; import { Widget } from '../Widget'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { NoticeBox } from '../NoticeBox'; @@ -104,6 +105,7 @@ const WidgetProfilePlain = ({ userRoles, } = useUserRoles(); const trackedEntityTypeName = program?.trackedEntityType?.displayName; + const attributes = useProgramLabel('attribute', { plural: true, programId }) ?? i18n.t('attributes'); const hasNoAttributes = !program?.programTrackedEntityAttributes?.length; @@ -174,11 +176,15 @@ const WidgetProfilePlain = ({

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

      ); 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..efc136a20f 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx @@ -9,6 +9,7 @@ import { } from '../../FormFields/New'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; +import { useProgramLabel } from '../../../metaData'; import type { ErrorMessagesForRelatedStages } from '../RelatedStagesActions'; import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; @@ -49,6 +50,7 @@ export const OrgUnitSelectorForRelatedStages = ({ saveAttempted, }: Props) => { const [touched, setTouched] = useState(false); + const orgUnit = useProgramLabel('orgUnit') ?? i18n.t('Organisation unit'); const handleSelect = (event: OrgUnitValue) => { setTouched(true); @@ -64,7 +66,7 @@ export const OrgUnitSelectorForRelatedStages = ({ return ( { const [touched, setTouched] = useState(false); + const event = useStageLabel('event') ?? i18n.t('event'); const handleChange = (value: string | null) => { setTouched(true); @@ -42,13 +44,14 @@ export const LinkToExisting = ({ setTouched(true); }; - const options = linkableEvents.map(event => ({ - value: event.id, - label: event.label, + const options = linkableEvents.map(linkableEvent => ({ + value: linkableEvent.id, + label: linkableEvent.label, })); - const label = i18n.t('Choose a {{linkableStageLabel}} event', { + const label = i18n.t('Choose a {{linkableStageLabel}} {{event}}', { linkableStageLabel, + event, }); const shouldShowError = (saveAttempted || touched); @@ -61,7 +64,7 @@ export const LinkToExisting = ({ onChange={handleChange} onBlur={handleBlur} options={options} - placeholder={i18n.t('Select an event')} + placeholder={i18n.t('Select {{event}}', { event })} 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..65ae5c0b9b 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 @@ -8,6 +8,7 @@ import { useCanAddNewEventToStage } from '../hooks'; import { DataSection } from '../../DataSection'; import { ScheduleInOrgUnit } from '../ScheduleInOrgUnit'; import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollection/helpers'; +import { useStageLabel, useProgramLabel } from '../../../metaData'; import type { PlainProps, LinkButtonProps } from './RelatedStagesActions.types'; import { LinkToExisting } from '../LinkToExisting'; import { EnterDataInOrgUnit } from '../EnterDataInOrgUnit/EnterData.component'; @@ -48,6 +49,7 @@ const Schedule = ({ updateSelectedAction, programStage, canAddNewEventToStage, + eventLabel, }) => { const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.SCHEDULE_IN_ORG] || {}; @@ -60,8 +62,9 @@ const Schedule = ({ if (disabled) { tooltipContent = disabledMessage; } else { - tooltipContent = i18n.t('{{ linkableStageLabel }} can only have one event', { + tooltipContent = i18n.t('{{ linkableStageLabel }} can only have one {{event}}', { linkableStageLabel: programStage.stageForm.name, + event: eventLabel, interpolation: { escapeValue: false }, }); } @@ -92,6 +95,7 @@ const EnterData = ({ updateSelectedAction, programStage, canAddNewEventToStage, + eventLabel, }) => { const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.ENTER_DATA] || {}; @@ -104,8 +108,9 @@ const EnterData = ({ if (disabled) { tooltipContent = disabledMessage; } else { - tooltipContent = i18n.t('{{ linkableStageLabel }} can only have one event', { + tooltipContent = i18n.t('{{ linkableStageLabel }} can only have one {{event}}', { linkableStageLabel: programStage.stageForm.name, + event: eventLabel, interpolation: { escapeValue: false }, }); } @@ -136,6 +141,7 @@ const LinkExistingResponse = ({ selectedAction, updateSelectedAction, programStage, + eventsLabel, }) => { const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.LINK_EXISTING_RESPONSE] || {}; @@ -148,8 +154,9 @@ const LinkExistingResponse = ({ if (disabled) { tooltipContent = disabledMessage; } else if (!linkableEvents.length) { - tooltipContent = i18n.t('{{ linkableStageLabel }} has no linkable events', { + tooltipContent = i18n.t('{{ linkableStageLabel }} has no linkable {{events}}', { linkableStageLabel: programStage.stageForm.name, + events: eventsLabel, interpolation: { escapeValue: false }, }); } @@ -190,6 +197,12 @@ const LinkButton = withStyles(styles)(({ ); }); +const useRelatedStagesLabels = (stageId?: string, programId?: string) => ({ + eventLabel: useStageLabel('event', { stageId, programId }) ?? i18n.t('event'), + eventsLabel: useStageLabel('event', { stageId, programId, plural: true }) ?? i18n.t('events'), + relationshipsLabel: useProgramLabel('relationship', { programId, plural: true }) ?? i18n.t('relationships'), +}); + const RelatedStagesActionsPlain = ({ classes, type, @@ -207,6 +220,9 @@ const RelatedStagesActionsPlain = ({ isLinking, }: PlainProps & WithStyles) => { const { programStage } = useProgramStageInfo(constraint?.programStage?.id); + const stageId = constraint?.programStage?.id; + const programId = constraint?.programStage?.program?.id; + const { eventLabel, eventsLabel, relationshipsLabel } = useRelatedStagesLabels(stageId, programId); const selectedAction = useMemo(() => relatedStagesDataValues.linkMode, [relatedStagesDataValues.linkMode]); @@ -236,6 +252,7 @@ const RelatedStagesActionsPlain = ({ updateSelectedAction={updateSelectedAction} programStage={programStage} canAddNewEventToStage={canAddNewEventToStage} + eventLabel={eventLabel} /> )} {type === relatedStageStatus.AMBIGUOUS_RELATIONSHIPS && ( -
      {i18n.t('Ambiguous relationships, contact system administrator')}
      +
      {i18n.t('Ambiguous {{relationships}}, contact system administrator', { + relationships: relationshipsLabel, + })}
      )}
      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..11621458ef 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -15,6 +15,7 @@ import { import { relatedStageStatus } from './constants'; import { useCommonEnrollmentDomainData } from '../Pages/common/EnrollmentOverviewDomain'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useStageLabel } from '../../metaData'; import type { RequestEvent } from '../DataEntries'; const styles = { @@ -54,6 +55,7 @@ export const WidgetRelatedStagesPlain = ({ const stageWriteAccess = Boolean(stageWriteAccessById[programStageId]); const linkedStageId = constraint?.programStage?.id; const linkedStageWriteAccess = linkedStageId ? Boolean(stageWriteAccessById[linkedStageId]) : false; + const event = useStageLabel('event', { stageId: linkedStageId, programId }) ?? i18n.t('event'); const { linkedEvent, isLoading: isLinkedEventLoading, @@ -123,7 +125,7 @@ export const WidgetRelatedStagesPlain = ({ - {i18n.t('Linked event')} + {i18n.t('Linked {{event}}', { event })}
      } > 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..42f9e886bb 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts @@ -2,6 +2,7 @@ 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 { useStageLabel } from '../../../metaData'; const ReactQueryAppNamespace = 'capture'; @@ -28,6 +29,7 @@ export const useAddEventWithRelationship = ({ }) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const event = useStageLabel('event') ?? i18n.t('event'); const { show: showSuccess } = useAlert(({ message }) => message, { success: true }); const { show: showAlert } = useAlert(({ message }) => message, { critical: true }); @@ -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: i18n.t('The {{event}} was successfully linked', { event }) }); } }, onError: (_, payload: { serverData: Record }) => { setIsLinking(false); - showAlert({ message: i18n.t('An error occurred while linking the event') }); + showAlert({ message: i18n.t('An error occurred while linking the {{event}}', { event }) }); 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 891650a162..8f4b8f3e5b 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,7 @@ import React, { useMemo } from 'react'; import { Button, IconAdd16 } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../../../../Tooltips/ConditionalTooltip'; +import { useStageLabel } from '../../../../../metaData'; type Props = { onCreateNew: () => void; @@ -18,12 +19,16 @@ export const StageCreateNewButton = ({ preventAddingEventActionInEffect, eventName, }: Props) => { + const event = useStageLabel('event') ?? i18n.t('event'); + const eventsPlural = useStageLabel('event', { plural: true }) ?? i18n.t('events'); + const stageLabel = useStageLabel('programStage') ?? i18n.t('stage'); const { isDisabled, tooltipContent } = useMemo(() => { if (preventAddingEventActionInEffect) { return { isDisabled: true, - tooltipContent: i18n.t("You can't add any more {{ programStageName }} events", { + tooltipContent: i18n.t("You can't add any more {{ programStageName }} {{events}}", { programStageName: eventName, + events: eventsPlural, interpolation: { escapeValue: false }, }), }; @@ -31,14 +36,18 @@ export const StageCreateNewButton = ({ if (!repeatable && eventCount > 0) { return { isDisabled: true, - tooltipContent: i18n.t('This stage can only have one event'), + tooltipContent: i18n.t('This {{stage}} can only have one {{event}}', { + stage: stageLabel, + event, + interpolation: { escapeValue: false }, + }), }; } return { isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, event, eventsPlural, stageLabel]); return ( - {i18n.t('New {{ eventName }} event', { - eventName, interpolation: { escapeValue: false }, + {i18n.t('New {{ eventName }} {{event}}', { + eventName, event, interpolation: { escapeValue: false }, })} 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 b3985371b5..7aaaa6188e 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 @@ -9,7 +9,7 @@ import { pipe } from 'capture-core-utils'; import { ConditionalTooltip } from '../../../../../../Tooltips/ConditionalTooltip'; import { isValidPeriod } from '../../../../../../../utils/validation/validators/form'; import { convertClientToView, convertServerToClient } from '../../../../../../../converters'; -import { dataElementTypes } from '../../../../../../../metaData'; +import { dataElementTypes, useStageLabel } from '../../../../../../../metaData'; const convertFn = pipe(convertServerToClient, convertClientToView); type Props = { @@ -30,11 +30,13 @@ export const DeleteActionButton = ({ }: Props) => { const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); const occurredAtClientView = convertFn(occurredAt, dataElementTypes.DATE); + const event = useStageLabel('event') ?? i18n.t('Event'); return ( { + const event = useProgramLabel('event', { programId }) ?? i18n.t('event'); const { show: showError } = useAlert( ({ message }) => message, { @@ -69,13 +71,13 @@ export const DeleteActionModal = ({ const eventToRollbackOnFail = previousData ?.enrollments ?.flatMap(enrollment => enrollment.events || []) - ?.find(event => event.event === eventId); + ?.find(eventItem => eventItem.event === eventId); onDeleteEvent(eventId); return eventToRollbackOnFail; }, onError: (apiError: unknown, payload: unknown, eventToRollbackOnFail?: any) => { - showError({ message: i18n.t('An error occurred while deleting the event') }); + showError({ message: i18n.t('An error occurred while deleting the {{event}}', { event }) }); log.error(errorCreator('An error occurred while deleting the event')({ apiError, payload })); if (eventToRollbackOnFail) { @@ -91,13 +93,13 @@ export const DeleteActionModal = ({ small > - {i18n.t('Delete event')} + {i18n.t('Delete {{event}}', { event })}

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

      @@ -111,7 +113,7 @@ export const DeleteActionModal = ({ destructive onClick={() => !pendingApiResponse && mutate({ eventId })} > - {i18n.t('Yes, delete event')} + {i18n.t('Yes, delete {{event}}', { event })} 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..93cb5d82b3 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 @@ -10,6 +10,7 @@ import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; import { DirectionalArrow } from '../../../../../../../utils/rtl'; +import { useStageLabel } from '../../../../../../../metaData'; import { EventStatuses } from '../EventRow'; type Props = { @@ -28,6 +29,7 @@ export const SkipAction = ({ onUpdateEventStatus, }: Props) => { const dataEngine = useDataEngine(); + const event = useStageLabel('event') ?? i18n.t('event'); const { show: showError } = useAlert( ({ message }) => message, { critical: true }, @@ -56,7 +58,10 @@ 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: i18n.t('An error occurred when updating {{event}} status', { + event, + interpolation: { escapeValue: false }, + }) }); 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 f6feb0ce38..84a6d41001 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,7 @@ 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 { useStageLabel } from '../../../../../metaData'; import type { Props } from './stageDetail.types'; import { EventRow } from './EventRow'; import { useClientDataElements } from './hooks/useClientDataElements'; @@ -107,6 +108,8 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); + const eventSingular = useStageLabel('event', { stageId, programId }) ?? i18n.t('event'); + const eventsPlural = useStageLabel('event', { plural: true, stageId, programId }) ?? i18n.t('Events'); const { stageWriteAccessById } = useEnrollmentAccessContext(); const stageWriteAccess = stageWriteAccessById[stageId] ?? stage?.access?.data?.write; const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); @@ -180,7 +183,10 @@ const StageDetailPlain = (props: Props & WithStyles) => { const cells = headerColumns.map(({ id }) => ( {({ onMouseOver, onMouseOut, ref }) => ( @@ -282,7 +288,10 @@ const StageDetailPlain = (props: Props & WithStyles) => { if (error) { return (
      - {i18n.t('Events could not be retrieved. Please try again later.')} + {i18n.t('{{events}} could not be retrieved. Please try again later.', { + events: eventsPlural, + interpolation: { escapeValue: false }, + })}
      ); } 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..f832bb587b 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,7 @@ 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, useStageLabel } from '../../../../../metaData'; const styles: Readonly = { container: { @@ -92,11 +92,17 @@ const getLastUpdatedAt = (events: Array, fromServerDate: (da return null; }; +const useEventLabels = () => ({ + eventSingular: useStageLabel('event') ?? i18n.t('event'), + eventPlural: useStageLabel('event', { plural: true }) ?? i18n.t('events'), +}); + export const StageOverviewPlain = ({ title, icon, description, events, stageWriteAccess = true, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); const { anyStageWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); + const { eventSingular, eventPlural } = useEventLabels(); const showStageBadge = showWidgetBadge && anyStageWriteAccess; const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; @@ -136,10 +142,10 @@ export const StageOverviewPlain = ({
      - {i18n.t('{{ count }} event', { + {i18n.t('{{ count }} {{eventLabel}}', { count: totalEvents, - defaultValue: '{{ count }} event', - defaultValue_plural: '{{count}} events', + eventLabel: totalEvents === 1 ? eventSingular : eventPlural, + interpolation: { escapeValue: false }, })}
      {overdueEvents > 0 ?
      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..4174c62939 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 @@ -6,6 +6,7 @@ import { Stage } from './Stage'; import type { PlainProps, InputProps } from './stages.types'; import { withLoadingIndicator } from '../../../HOC'; import { useEnrollmentAccessContext } from '../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useStageLabel } from '../../../metaData'; const emptyStateStyle = { padding: `0 ${spacersNum.dp12}px`, @@ -23,6 +24,7 @@ export const StagesPlain = ({ ...passOnProps }: PlainProps) => { const { stageReadAccessById } = useEnrollmentAccessContext(); + const stagesLabel = useStageLabel('programStage', { plural: true }) ?? i18n.t('stages'); const readableStages = useMemo( () => stages.filter(stage => stageReadAccessById[stage.id] ?? stage.dataAccess.read), [stages, stageReadAccessById], @@ -52,7 +54,10 @@ export const StagesPlain = ({ if (!readableStages.length) { return (

      - {i18n.t('No stages found in this program')} + {i18n.t('No {{stages}} found in this program', { + stages: stagesLabel, + interpolation: { escapeValue: false }, + })}

      ); } 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..d5064e8f9c 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx @@ -6,6 +6,7 @@ import { Widget } from '../Widget'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { Stages } from './Stages'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { useStageLabel } from '../../metaData'; import type { Props } from './stagesAndEvents.types'; const styles = { @@ -35,6 +36,8 @@ const WidgetStagesAndEventsPlain = ({ multipleStages, showWidgetBadge, } = useEnrollmentAccessContext(); + const stagesLabel = useStageLabel('programStage', { plural: true, programId }) ?? i18n.t('Stages'); + const eventsLabel = useStageLabel('event', { plural: true, programId }) ?? i18n.t('Events'); return (
      - {i18n.t('Stages and Events')} + {i18n.t('{{stages}} and {{events}}', { + stages: stagesLabel, + events: eventsLabel, + interpolation: { escapeValue: false }, + })} {showWidgetBadge && (
      { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const event = useStageLabel('event') ?? i18n.t('event'); const { show: showErrorAlert } = useAlert( - i18n.t('An error occurred while unlinking and deleting the event.'), + i18n.t('An error occurred while unlinking and deleting the {{event}}.', { + event, + interpolation: { escapeValue: false }, + }), { critical: true }, ); @@ -61,12 +66,21 @@ export const UnlinkAndDeleteModal = ({ return ( - {i18n.t('Unlink and delete linked event')} + {i18n.t('Unlink and delete linked {{event}}', { + event, + interpolation: { escapeValue: false }, + })}

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

      @@ -82,7 +96,10 @@ export const UnlinkAndDeleteModal = ({ onClick={() => mutation.mutate()} disabled={mutation.isLoading} > - {i18n.t('Yes, unlink and delete linked event')} + {i18n.t('Yes, unlink and delete linked {{event}}', { + event, + interpolation: { escapeValue: false }, + })} 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..220e6cd253 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 @@ -12,6 +12,7 @@ import log from 'loglevel'; import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers'; +import { useStageLabel } from '../../../../metaData'; import type { Props } from './UnlinkModal.types'; export const UnlinkModal = ({ @@ -22,8 +23,13 @@ export const UnlinkModal = ({ }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const event = useStageLabel('event') ?? i18n.t('event'); + const eventsPlural = useStageLabel('event', { plural: true }) ?? i18n.t('events'); const { show: showErrorAlert } = useAlert( - i18n.t('An error occurred while unlinking and deleting the event.'), + i18n.t('An error occurred while unlinking and deleting the {{event}}.', { + event, + interpolation: { escapeValue: false }, + }), { critical: true }, ); @@ -59,13 +65,22 @@ export const UnlinkModal = ({ return ( - {i18n.t('Unlink event')} + {i18n.t('Unlink {{event}}', { + event, + interpolation: { escapeValue: false }, + })}

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

      @@ -79,7 +94,10 @@ export const UnlinkModal = ({ disabled={mutation.isLoading} dataTest="event-overflow-unlink-event-confirm" > - {i18n.t('Yes, unlink event')} + {i18n.t('Yes, unlink {{event}}', { + event, + interpolation: { escapeValue: false }, + })} 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..6d3965e8a5 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 @@ -15,6 +15,7 @@ import { UnlinkModal, UnlinkAndDeleteModal } from './Modal'; import { useNavigate, buildUrlQueryString } from '../../../utils/routing'; import type { Props } from './OverflowMenu.types'; import { useRelationshipTypeAccess } from '../hooks'; +import { useStageLabel } from '../../../metaData'; export const OverflowMenuComponent = ({ linkedEvent, @@ -27,6 +28,8 @@ export const OverflowMenuComponent = ({ onDeleteEventRelationship, }: Props) => { const { navigate } = useNavigate(); + const event = useStageLabel('event') ?? i18n.t('event'); + const eventsPlural = useStageLabel('event', { plural: true }) ?? i18n.t('events'); const [isActionsOpen, setIsActionsOpen] = useState(false); const [isUnlinkModalOpen, setIsUnlinkModalOpen] = useState(false); const [isUnlinkAndDeleteModalOpen, setIsUnlinkAndDeleteModalOpen] = useState(false); @@ -59,7 +62,10 @@ export const OverflowMenuComponent = ({ component={ } dataTest="event-overflow-view-linked-event" onClick={handleViewLinkedEvent} @@ -67,11 +73,17 @@ export const OverflowMenuComponent = ({ /> } disabled={!stageWriteAccess || !relationshipTypeWriteAccess} dense @@ -81,11 +93,17 @@ 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..0d6c4a81f4 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 @@ -4,6 +4,7 @@ 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 { useStageLabel } from '../../../metaData'; export const styles: Readonly = { container: { @@ -40,6 +41,7 @@ export const styles: Readonly = { }; const WidgetWrapperPlain = ({ widget, type, stage, linkedStage, classes }: PlainProps & WithStyles) => { + const event = useStageLabel('event') ?? i18n.t('event'); if (type === WidgetTwoEventWorkspaceWrapperTypes.EDIT_EVENT) { return (
      @@ -49,15 +51,19 @@ const WidgetWrapperPlain = ({ widget, type, stage, linkedStage, classes }: Plain -
      {i18n.t('Linked event')}
      +
      {i18n.t('Linked {{event}}', { + event, + interpolation: { escapeValue: false }, + })}
      {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', + i18n.t('This {{stageName}} {{event}} is linked to a {{linkedStageName}} {{event}}. Review the linked {{event}} details before entering data below', { linkedStageName: linkedStage.name, stageName: stage.name, + event, interpolation: { escapeValue: false }, }, ) : ''} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx index 9d34dc9ed9..8e96f4ee1e 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n'; import { colors, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from '../wizardSteps.const'; +import { useProgramLabel } from '../../../../../metaData'; import type { PlainProps } from './breadcrumbs.types'; const styles = { @@ -43,8 +44,10 @@ const Slash = withStyles(slashStyles)(({ classes }: WithStyles/); const LinkedEntityMetadataSelectorStep = ({ currentStep, onNavigate, trackedEntityTypeName }: any) => { - const initialText = i18n.t('New {{trackedEntityTypeName}} relationship', { + const relationship = useProgramLabel('relationship') ?? i18n.t('relationship'); + const initialText = i18n.t('New {{trackedEntityTypeName}} {{relationship}}', { trackedEntityTypeName: trackedEntityTypeName && trackedEntityTypeName.toLowerCase(), + relationship, }); return (currentStep.value > NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.value ? {initialText} : 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..dc5d929cd5 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 @@ -14,6 +14,7 @@ import type { ComponentProps } from './NewTrackedEntityRelationship.types'; import { useAddRelationship } from './hooks/useAddRelationship'; import { TARGET_SIDES } from './common'; import { generateUID } from '../../../../utils/uid/generateUID'; +import { useProgramLabel } from '../../../../metaData'; const styles = { container: { @@ -58,6 +59,7 @@ const NewTrackedEntityRelationshipPlain = ({ teiId, onMutate: () => onSave && onSave(), }); + const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('relationship'); const onLinkToTrackedEntityFromSearch = useCallback( @@ -275,7 +277,7 @@ const NewTrackedEntityRelationshipPlain = ({
      - {i18n.t('Go back without saving relationship')} + {i18n.t('Go back without saving {{relationship}}', { relationship })}
      ) => { const [addWizardVisible, setAddWizardVisible] = useState(false); + const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('Relationship'); const closeAddWizard = useCallback(() => { setAddWizardVisible(false); @@ -48,7 +50,7 @@ const NewTrackedEntityRelationshipPlain = ({ small secondary > - {i18n.t('New Relationship')} + {i18n.t('New {{relationship}}', { relationship })} )} 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 51ef806aca..9fcf00a193 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 @@ -4,6 +4,7 @@ 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 { useProgramLabel } from '../../../../../metaData'; type Props = { teiId: string; @@ -23,8 +24,9 @@ export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { const queryClient = useQueryClient(); const queryKey: string = useFeature(FEATURES.exportablePayload) ? 'relationships' : 'instances'; const dataEngine = useDataEngine(); + const relationshipLabel = useProgramLabel('relationship') ?? i18n.t('relationship'); const { show: showAlert } = useAlert( - i18n.t('An error occurred while adding the relationship'), + i18n.t('An error occurred while adding the {{relationship}}', { relationship: 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..6ec60384f1 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,5 +1,6 @@ import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; +import { useProgramLabel } from '../../../metaData'; import type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; import { RelationshipsWidget } from '../common/RelationshipsWidget'; import { RelationshipSearchEntities, useRelationships } from '../common/useRelationships'; @@ -40,11 +41,14 @@ export const WidgetTrackedEntityRelationship = ({ const isLoading = useMemo(() => isLoadingRelationships || isLoadingTEType, [isLoadingRelationships, isLoadingTEType], ); + const relationshipsLabel = useProgramLabel('relationship', { plural: true, programId }) ?? i18n.t('relationships'); if (isError) { return (
      - {i18n.t('Something went wrong while loading relationships. Please try again later.')} + {i18n.t('Something went wrong while loading {{relationships}}. Please try again later.', { + relationships: relationshipsLabel, + })}
      ); } @@ -55,8 +59,9 @@ export const WidgetTrackedEntityRelationship = ({ return ( = { @@ -28,6 +29,7 @@ export const DeleteRelationshipPlain = ({ classes, }: Props & WithStyles) => { const [isModalOpen, setIsModalOpen] = useState(false); + const relationship = useProgramLabel('relationship') ?? i18n.t('relationship'); return ( <> @@ -48,11 +50,11 @@ export const DeleteRelationshipPlain = ({ onClose={() => setIsModalOpen(false)} dataTest={'delete-relationship-modal'} > - {i18n.t('Delete relationship')} + {i18n.t('Delete {{relationship}}', { relationship })} - {i18n.t('Deleting the relationship is permanent and cannot be undone.')} + {i18n.t('Deleting the {{relationship}} is permanent and cannot be undone.', { relationship })} {' '} - {i18n.t('Are you sure you want to delete this relationship?')} + {i18n.t('Are you sure you want to delete this {{relationship}}?', { relationship })} @@ -69,7 +71,7 @@ export const DeleteRelationshipPlain = ({ setIsModalOpen(false); }} > - {i18n.t('Yes, delete relationship')} + {i18n.t('Yes, delete {{relationship}}', { relationship })} 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 48305ec002..d1cfb929f2 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 @@ -5,6 +5,7 @@ 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 { useProgramLabel } from '../../../../../metaData'; type Props = { sourceId: string; @@ -28,8 +29,9 @@ export const useDeleteRelationship = ({ sourceId }: Props): { onDeleteRelationsh const dataEngine = useDataEngine(); const queryKey: string = useFeature(FEATURES.exportablePayload) ? 'relationships' : 'instances'; const queryClient = useQueryClient(); + const relationship = useProgramLabel('relationship') ?? i18n.t('relationship'); const { show: showError } = useAlert( - i18n.t('An error occurred while deleting the relationship.'), + i18n.t('An error occurred while deleting the {{relationship}}.', { relationship }), { critical: true, }, @@ -45,7 +47,7 @@ export const useDeleteRelationship = ({ sourceId }: Props): { onDeleteRelationsh const apiRelationships = handleAPIResponse(REQUESTED_ENTITIES.relationships, prevRelationships); const newRelationships = apiRelationships - ?.filter(({ relationship }: any) => relationship !== relationshipId); + ?.filter((rel: any) => rel.relationship !== relationshipId); queryClient.setQueryData( [ReactQueryAppNamespace, 'relationships', sourceId], 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..1c1ed73b68 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 @@ -5,6 +5,7 @@ import { withStyles } from 'capture-core-utils/styles'; import type { WithStyles } from 'capture-core-utils/styles'; import { Widget, WidgetHeaderCountBadge } from '../../../Widget'; import { ReadOnlyBadge } from '../../../ReadOnlyBadge'; +import { useProgramLabel } from '../../../../metaData'; import { useGroupedLinkedEntities } from './useGroupedLinkedEntities'; import { LinkedEntitiesViewer } from './LinkedEntitiesViewer.component'; import type { Props } from './relationshipsWidget.types'; @@ -36,6 +37,8 @@ const RelationshipsWidgetPlain = ({ classes, }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); + const enrollmentLabel = useProgramLabel('enrollment') ?? i18n.t('enrollment'); + const relationshipsLabel = useProgramLabel('relationship', { plural: true }) ?? i18n.t('relationships'); const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships, readOnly); const { onDeleteRelationship } = useDeleteRelationship({ sourceId }); @@ -92,7 +95,10 @@ const RelationshipsWidgetPlain = ({ } {(relationships?.length ?? 0) === 0 && (
      - {i18n.t("This enrollment doesn't have any relationships")} + {i18n.t("This {{enrollment}} doesn't have any {{relationships}}", { + enrollment: enrollmentLabel, + relationships: relationshipsLabel, + })}
      )} {children} 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..24d89349cd 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,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } from '@dhis2/ui'; +import { useProgramLabel } from '../../../../metaData'; type Props = { eventId: string; @@ -9,6 +10,7 @@ type Props = { }; export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) => { + const event = useProgramLabel('event') ?? i18n.t('event'); const handleConfirm = () => { onConfirmDelete(eventId); onClose(); @@ -20,13 +22,13 @@ export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) = small > - {i18n.t('Delete event')} + {i18n.t('Delete {{event}}', { event })}

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

      @@ -40,7 +42,7 @@ export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) = destructive onClick={handleConfirm} > - {i18n.t('Yes, delete event')} + {i18n.t('Yes, delete {{event}}', { event })} 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..9755854356 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 @@ -6,11 +6,13 @@ import type { CustomRowMenuContents } from '../../WorkingListsBase'; import type { Props } from './eventWorkingListsRowMenuSetup.types'; import { useProgramExpiryForUser } from '../../../../hooks'; import { isValidPeriod } from '../../../../utils/validation/validators/form'; +import { useProgramLabel } from '../../../../metaData'; import { DeleteEventModal } from './DeleteEventModal'; export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...passOnProps }: Props) => { const expiryPeriod = useProgramExpiryForUser(programId); + const event = useProgramLabel('event', { programId }) ?? i18n.t('event'); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [eventIdToDelete, setEventIdToDelete] = useState(null); @@ -32,7 +34,7 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas key: 'deleteEventItem', clickHandler: ({ id }) => handleOpenDeleteModal(id), icon: , - label: i18n.t('Delete event'), + label: i18n.t('Delete {{event}}', { event }), tooltipContent: (row) => { const { occurredAt } = row ?? {}; const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); @@ -54,7 +56,7 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); return !isWithinValidPeriod; }, - }], [expiryPeriod]); + }], [expiryPeriod, event]); return ( 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..6d6e92147f 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 @@ -5,6 +5,7 @@ import { Button, ButtonStrip, colors, Modal, ModalActions, ModalContent, ModalTi import { useBulkCompleteEvents } from './hooks/useBulkCompleteEvents'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import { Widget } from '../../../../../Widget'; +import { useProgramLabel } from '../../../../../../metaData'; import type { Props } from './CompleteAction.types'; const styles: Readonly = { @@ -21,9 +22,9 @@ const styles: Readonly = { }, }; -const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { +const getTooltipContent = (events: string, stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { if (!stageDataWriteAccess) { - return i18n.t('You do not have access to complete events'); + return i18n.t('You do not have access to complete {{events}}', { events }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -31,6 +32,9 @@ const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive return ''; }; +const useEventsLabel = (programId?: string) => + useProgramLabel('event', { plural: true, programId }) ?? i18n.t('events'); + const CompleteActionPlain = ({ selectedRows, stageDataWriteAccess, @@ -42,7 +46,8 @@ const CompleteActionPlain = ({ }: Props & WithStyles) => { const [isCompleteDialogOpen, setIsCompleteDialogOpen] = useState(false); const [openAccordion, setOpenAccordion] = useState(false); - const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive); + const events = useEventsLabel(programId); + const tooltipContent = getTooltipContent(events, stageDataWriteAccess, bulkDataEntryIsActive); const disabled = Boolean(!stageDataWriteAccess || bulkDataEntryIsActive); const { eventCounts, @@ -81,15 +86,17 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-events-dialog'} > - {i18n.t('Complete events')} + {i18n.t('Complete {{events}}', { events })} {eventCounts.active > 0 ? - i18n.t('Are you sure you want to complete all active events in selection?') + // eslint-disable-next-line max-len + i18n.t('Are you sure you want to complete all active {{events}} in selection?', { events }) : - i18n.t('There are no active events to complete in the current selection.') + // eslint-disable-next-line max-len + i18n.t('There are no active {{events}} to complete in the current selection.', { events }) } @@ -124,12 +131,12 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-events-dialog'} > - {i18n.t('Error completing events')} + {i18n.t('Error completing {{events}}', { events })} - {i18n.t('There was an error completing the events.')} + {i18n.t('There was an error completing the {{events}}.', { events })} { const dataEngine = useDataEngine(); + const eventsLabel = useProgramLabel('event', { plural: true, programId }) ?? i18n.t('events'); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, @@ -81,7 +83,7 @@ export const useBulkCompleteEvents = ({ }), { onError: () => { - showAlert({ message: i18n.t('An error occurred while completing events') }); + showAlert({ message: i18n.t('An error occurred while completing {{events}}', { events: 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..4294d14f4d 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 @@ -6,11 +6,12 @@ import { useMutation } from '@tanstack/react-query'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator } from 'capture-core-utils'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; +import { useProgramLabel } from '../../../../../../metaData'; import type { Props } from './DeleteAction.types'; -const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { +const getTooltipContent = (events: string, stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { if (!stageDataWriteAccess) { - return i18n.t('You do not have access to delete events'); + return i18n.t('You do not have access to delete {{events}}', { events }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -26,12 +27,13 @@ export const DeleteAction = ({ }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); const dataEngine = useDataEngine(); + const events = useProgramLabel('event', { plural: true }) ?? i18n.t('events'); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, ); - const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive); + const tooltipContent = getTooltipContent(events, stageDataWriteAccess, bulkDataEntryIsActive); const disabled = Boolean(!stageDataWriteAccess || !!bulkDataEntryIsActive); const { mutate: deleteEvents, isLoading }: { mutate: any, isLoading: boolean } = useMutation( @@ -47,7 +49,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: i18n.t('An error occurred while deleting the {{events}}', { events }) }); }, onSuccess: () => { onUpdateList(); @@ -78,13 +80,13 @@ export const DeleteAction = ({ dataTest={'bulk-delete-events-dialog'} > - {i18n.t('Delete events')} + {i18n.t('Delete {{events}}', { events })} {i18n.t('This cannot be undone.')} {' '} - {i18n.t('Are you sure you want to delete the selected events?')} + {i18n.t('Are you sure you want to delete the selected {{events}}?', { events })} 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..486072ad72 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 @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { translatedStatusTypes } from 'capture-core/events/statusTypes'; import i18n from '@dhis2/d2-i18n'; import type { ProgramStage } from '../../../../metaData'; -import { dataElementTypes as elementTypeKeys } from '../../../../metaData'; +import { dataElementTypes as elementTypeKeys, useProgramLabel } from '../../../../metaData'; import { mainPropertyNames } from '../../../../events/mainPropertyNames.const'; import type { MainColumnConfig, @@ -11,7 +11,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 +21,7 @@ const getDefaultMainConfig = (stage: ProgramStage): Array => { id: mainPropertyNames.ORGANISATION_UNIT, visible: true, type: elementTypeKeys.ORGANISATION_UNIT, - header: i18n.t('Organisation unit'), + header: orgUnitLabel, apiName: 'orgUnit', filterHidden: true, }, { @@ -66,8 +66,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 = useProgramLabel('orgUnit') ?? i18n.t('Organisation unit'); + 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 4ec2d5c1c5..004f37a39a 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,19 @@ 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, useStageLabel, type TrackerProgram } from '../../../../../metaData'; import type { MainColumnConfig, MetadataColumnConfig, TrackerWorkingListsColumnConfigs } from '../../types'; -const getMainConfig = (hasDisplayInReportsAttributes: boolean): Array => +const getMainConfig = ( + hasDisplayInReportsAttributes: boolean, + ownerOrgUnitLabel: string, +): Array => [ { id: 'programOwnerId', visible: false, type: dataElementTypes.ORGANISATION_UNIT, - header: i18n.t('Owner organisation unit'), + header: ownerOrgUnitLabel, sortDisabled: true, filterHidden: true, apiViewName: 'programOwner', @@ -34,13 +37,17 @@ const getMainConfig = (hasDisplayInReportsAttributes: boolean): Array => +const getProgramStageMainConfig = ( + programStage, + eventStatusLabel: string, + eventOrgUnitLabel: string, +): Array => [ { id: ADDITIONAL_FILTERS.status, visible: true, type: dataElementTypes.TEXT, - header: i18n.t(ADDITIONAL_FILTERS_LABELS.status), + header: eventStatusLabel, }, { id: ADDITIONAL_FILTERS.occurredAt, @@ -64,7 +71,7 @@ const getProgramStageMainConfig = (programStage): Array => id: ADDITIONAL_FILTERS.orgUnit, visible: true, type: dataElementTypes.ORGANISATION_UNIT, - header: ADDITIONAL_FILTERS_LABELS.orgUnit, + header: eventOrgUnitLabel, apiViewName: 'eventOrgUnit', }, ...(programStage.enableUserAssignment @@ -148,23 +155,34 @@ export const useDefaultColumnConfig = ( program: TrackerProgram, orgUnitId: string | null | undefined, programStageId: string | null | undefined, -): TrackerWorkingListsColumnConfigs => - useMemo(() => { +): TrackerWorkingListsColumnConfigs => { + const eventTerm = + useStageLabel('event', { programId: program.id, stageId: programStageId ?? undefined }) ?? i18n.t('Event'); + const orgUnitTerm = + useStageLabel('orgUnit', { programId: program.id, stageId: programStageId ?? undefined }) ?? + i18n.t('organisation unit'); + + const ownerOrgUnitLabel = i18n.t('Owner {{orgUnit}}', { orgUnit: orgUnitTerm }); + const eventStatusLabel = i18n.t('{{event}} status', { event: eventTerm }); + const eventOrgUnitLabel = i18n.t('{{event}} {{orgUnit}}', { event: eventTerm, orgUnit: orgUnitTerm }); + + 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, ownerOrgUnitLabel), ...getTEIMetaDataConfig(attributes, orgUnitId, searchFilterMetaById), ]; if (programStageId && programStage) { return defaultColumns.concat([ - ...getProgramStageMainConfig(programStage), + ...getProgramStageMainConfig(programStage, eventStatusLabel, eventOrgUnitLabel), ...getEventsMetaDataConfig(programStage), ]); } return defaultColumns; - }, [orgUnitId, program, programStageId]); + }, [orgUnitId, program, programStageId, ownerOrgUnitLabel, eventStatusLabel, eventOrgUnitLabel]); +}; 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..e56d932544 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,23 @@ 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, useProgramLabel, type TrackerProgram } from '../../../../../metaData'; import { MAIN_FILTERS } from '../../constants'; export const useFiltersOnly = ( - { enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, + { id, enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, programStageId?: string, -) => - useMemo(() => { +) => { + const enrollmentLabel = useProgramLabel('enrollment', { programId: id }) ?? i18n.t('Enrollment'); + const followUpLabel = useProgramLabel('followUp', { programId: id }) ?? i18n.t('Follow up'); + 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('{{enrollment}} status', { enrollment: enrollmentLabel }), options: [ { text: i18n.t('Active'), value: 'ACTIVE' }, { text: i18n.t('Completed'), value: 'COMPLETED' }, @@ -71,7 +73,7 @@ export const useFiltersOnly = ( { id: MAIN_FILTERS.FOLLOW_UP, type: dataElementTypes.BOOLEAN, - header: i18n.t('Follow up'), + header: followUpLabel, showInMoreFilters: true, multiValueFilter: false, transformRecordsFilter: (rawFilter: string) => ({ @@ -99,4 +101,5 @@ export const useFiltersOnly = ( ] : []), ]; - }, [enrollmentDateLabel, incidentDateLabel, showIncidentDate, stages, programStageId]); + }, [enrollmentDateLabel, incidentDateLabel, showIncidentDate, stages, programStageId, enrollmentLabel, followUpLabel]); +}; 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 413c067b05..fd9060628e 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 @@ -2,7 +2,13 @@ import { useMemo } from 'react'; import { featureAvailable, FEATURES } from 'capture-core-utils'; 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, + useStageLabel, +} from '../../../../../metaData'; import { ADDITIONAL_FILTERS, ADDITIONAL_FILTERS_LABELS } from '../../helpers'; const useProgramStageData = (programStageId, stages) => @@ -45,6 +51,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: program.stages, ); const options: Array<{ text: string, value: string }> = useProgramStageDropdowOptions(program.stages, program.id); + const programStageLabel = useStageLabel('programStage', { programId: program.id, stageId: programStageId }) + ?? i18n.t(ADDITIONAL_FILTERS_LABELS.programStage); return useMemo(() => { const translatedStatus = translatedStatusTypes(); @@ -52,7 +60,7 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: { id: ADDITIONAL_FILTERS.programStage, type: dataElementTypes.TEXT, - header: i18n.t(ADDITIONAL_FILTERS_LABELS.programStage), + header: programStageLabel, options, mainButton: true, transformRecordsFilter: () => null, @@ -148,5 +156,5 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: ] : []), ]; - }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options, enableUserAssignment]); + }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options, enableUserAssignment, programStageLabel]); }; 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..b087e81613 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 { useProgramLabel } from '../../../../../metaData'; import type { WorkingListTemplate } from '../../../WorkingListsBase'; -export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => - useMemo( +export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => { + const enrollments = useProgramLabel('enrollment', { plural: true }) ?? i18n.t('enrollments'); + 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 {{enrollments}}', { enrollments }), 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 {{enrollments}}', { enrollments }), 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 {{enrollments}}', { enrollments }), order: 3, access: { update: false, @@ -59,5 +61,6 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, }, ], - [defaultAlteredTemplate, defaultTemplateId], + [defaultAlteredTemplate, defaultTemplateId, enrollments], ); +}; 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..9d998522b6 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 @@ -15,6 +15,7 @@ import { import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import { useCompleteBulkEnrollments } from './hooks/useCompleteBulkEnrollments'; import { Widget } from '../../../../../Widget'; +import { useProgramLabel } from '../../../../../../metaData'; import type { PlainProps } from './CompleteAction.types'; const styles: Readonly = { @@ -36,9 +37,9 @@ const styles: Readonly = { }, }; -const getTooltipContent = (programDataWriteAccess: boolean, bulkDataEntryIsActive: boolean) => { +const getTooltipContent = (enrollments: string, programDataWriteAccess: boolean, bulkDataEntryIsActive: boolean) => { 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 {{enrollments}}', { enrollments }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -59,6 +60,8 @@ const CompleteActionPlain = ({ const [modalIsOpen, setModalIsOpen] = useState(false); const [completeEvents, setCompleteEvents] = useState(true); const [openAccordion, setOpenAccordion] = useState(false); + const enrollments = useProgramLabel('enrollment', { plural: true, programId }) ?? i18n.t('enrollments'); + const events = useProgramLabel('event', { plural: true, programId }) ?? i18n.t('events'); const { completeEnrollments, enrollmentCounts, @@ -75,7 +78,7 @@ const CompleteActionPlain = ({ onUpdateList, removeRowsFromSelection, }); - const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive); + const tooltipContent = getTooltipContent(enrollments, programDataWriteAccess, bulkDataEntryIsActive); const disabled = !programDataWriteAccess || bulkDataEntryIsActive; const ModalTextContent = () => { @@ -96,8 +99,9 @@ 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 {{enrollments}} were completed successfully, but there was an error while completing the rest. Please see the details below.', { enrollments }) : + // eslint-disable-next-line max-len + i18n.t('There was an error while completing the {{enrollments}}. Please see the details below.', { enrollments }) } @@ -129,7 +133,8 @@ const CompleteActionPlain = ({ if (errorFetchingTrackedEntities) { return (
      - {i18n.t('An unexpected error occurred while fetching the enrollments. Please try again.')} + {/* eslint-disable-next-line max-len */} + {i18n.t('An unexpected error occurred while fetching the {{enrollments}}. Please try again.', { enrollments })}
      ); } @@ -138,9 +143,9 @@ 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 {{enrollments}} in the selection.', { enrollments })} {' '} - {i18n.t('All enrollments are already completed or cancelled.')} + {i18n.t('All {{enrollments}} are already completed or cancelled.', { enrollments })}
      ); } @@ -166,7 +171,7 @@ const CompleteActionPlain = ({ } setCompleteEvents(prevState => !prevState)} /> @@ -186,7 +191,7 @@ const CompleteActionPlain = ({ disabled={disabled} onClick={() => setModalIsOpen(true)} > - {i18n.t('Complete enrollments')} + {i18n.t('Complete {{enrollments}}', { enrollments })} @@ -196,8 +201,8 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-enrollments-dialog'} > - {validationError ? i18n.t('Error completing enrollments') - : i18n.t('Complete enrollments')} + {validationError ? i18n.t('Error completing {{enrollments}}', { enrollments }) + : i18n.t('Complete {{enrollments}}', { enrollments })} @@ -215,7 +220,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 67b3b227f1..c90b6aa1af 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 @@ -4,6 +4,7 @@ import { Button, ButtonStrip, CircularLoader, Modal, ModalActions, ModalContent, import i18n from '@dhis2/d2-i18n'; import { useDeleteEnrollments } from '../hooks/useDeleteEnrollments'; import { CustomCheckbox } from './CustomCheckbox'; +import { useProgramLabel } from '../../../../../../../metaData'; import type { PlainProps } from './EnrollmentDeleteModal.types'; const styles: Readonly = { @@ -19,6 +20,12 @@ const styles: Readonly = { }, }; +const useEnrollmentDeleteLabels = (programId?: string) => ({ + enrollments: useProgramLabel('enrollment', { plural: true, programId }) ?? i18n.t('enrollments'), + enrollment: useProgramLabel('enrollment', { programId }) ?? i18n.t('enrollment'), + events: useProgramLabel('event', { plural: true, programId }) ?? i18n.t('events'), +}); + const EnrollmentDeleteModalPlain = ({ selectedRows, programId, @@ -26,6 +33,7 @@ const EnrollmentDeleteModalPlain = ({ setIsDeleteDialogOpen, classes, }: PlainProps & WithStyles) => { + const { enrollments, enrollment, events } = useEnrollmentDeleteLabels(programId); const { deleteEnrollments, isDeletingEnrollments, @@ -49,12 +57,13 @@ const EnrollmentDeleteModalPlain = ({ small > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollments}}', { enrollments })}
      - {i18n.t('An error occurred while loading the selected enrollments. Please try again.')} + {/* eslint-disable-next-line max-len */} + {i18n.t('An error occurred while loading the selected {{enrollments}}. Please try again.', { enrollments })}
      @@ -78,7 +87,7 @@ const EnrollmentDeleteModalPlain = ({ onClose={() => setIsDeleteDialogOpen(false)} > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollments}}', { enrollments })} @@ -107,24 +116,27 @@ const EnrollmentDeleteModalPlain = ({ dataTest={'bulk-delete-enrollments-dialog'} > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollments}}', { enrollments })}
      {/* 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 {{enrollments}}, including all associated data and {{events}}.', { enrollments, events })}
      - {i18n.t('Please select which enrollment statuses you want to delete:')} + {i18n.t('Please select which {{enrollment}} statuses you want to delete:', { enrollment })}
      ; @@ -29,6 +30,7 @@ export const useDeleteEnrollments = ({ cancelled: true, }); const dataEngine = useDataEngine(); + const enrollmentsLabel = useProgramLabel('enrollment', { plural: true, programId }) ?? i18n.t('enrollments'); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, @@ -86,7 +88,8 @@ 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 {{enrollments}}', { enrollments: enrollmentsLabel }) }); }, onSuccess: () => { queryClient.removeQueries([ReactQueryAppNamespace, ...QueryKey]); From 536969c62b77257c086619f371bad13be973cebc Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:03:43 +0200 Subject: [PATCH 07/15] feat: update terminology types and labels to support custom labels --- i18n/en.pot | 7 ++-- .../capture-core/metaData/Program/Program.ts | 1 + .../TrackedEntityType/TrackedEntityType.ts | 4 ++- .../helpers/customLabels/customLabels.ts | 34 +++++-------------- .../metaData/helpers/customLabels/index.ts | 2 +- .../metaData/helpers/customLabels/useLabel.ts | 11 +++--- .../capture-core/metaData/helpers/index.ts | 2 +- .../capture-core/metaData/index.ts | 2 +- 8 files changed, 22 insertions(+), 41 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d8b43b4854..b89585a062 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-06-14T13:31:02.618Z\n" -"PO-Revision-Date: 2026-06-14T13:31:02.618Z\n" +"POT-Creation-Date: 2026-06-10T13:04:43.537Z\n" +"PO-Revision-Date: 2026-06-10T13:04:43.537Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2210,9 +2210,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/Program/Program.ts b/src/core_modules/capture-core/metaData/Program/Program.ts index b0ad439274..d59b5c3fa2 100644 --- a/src/core_modules/capture-core/metaData/Program/Program.ts +++ b/src/core_modules/capture-core/metaData/Program/Program.ts @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ /* eslint-disable no-restricted-syntax */ + import isFunction from 'd2-utilizr/lib/isFunction'; import type { ProgramRule, ProgramRuleVariable } from '@dhis2/rules-engine-javascript'; import type{ CategoryCombination } from '../CategoryCombinations'; diff --git a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts index e3ba50563e..8457d90378 100644 --- a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts +++ b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable no-restricted-syntax */ + import isFunction from 'd2-utilizr/lib/isFunction'; import type { SearchGroup } from '../SearchGroup'; import type { DataElement } from '../DataElement'; @@ -5,7 +8,6 @@ import type { TeiRegistration } from './TeiRegistration'; import type { Access } from '../Access'; import type { CustomLabels } from '../helpers/customLabels'; -/* eslint-disable no-underscore-dangle */ export class TrackedEntityType { _id!: string; _access!: Access; 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 580c16023c..18938840cd 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,11 +1,4 @@ -/** - * Configurable terminology (DHIS2-16581 + v43 plurals). - * - * Each term maps to the backend field(s) that hold the custom (translated) label. - * The resolver returns the configured custom string, or `undefined` when none is - * set — callers fall back to their existing default term (`?? i18n.t('…')`). - */ -type TermDef = { +type CustomLabelField = { field?: string, pluralField?: string, }; @@ -20,21 +13,20 @@ export const CUSTOM_LABEL_FIELDS = { programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel' }, -} as const satisfies { [key: string]: TermDef }; +} as const satisfies { [key: string]: CustomLabelField }; -export type TermKey = keyof typeof CUSTOM_LABEL_FIELDS; +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: TermDef) => [term.field, term.pluralField]) + .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) .filter((field): field is string => Boolean(field)), ), ); -/** Copies the present custom-label values out of a cached metadata object. */ export const extractCustomLabels = (cached: Record): CustomLabels => { const labels: CustomLabels = {}; allFields.forEach((field) => { @@ -47,20 +39,12 @@ export const extractCustomLabels = (cached: Record): CustomLabels = type LabelSource = CustomLabels | undefined | null; -/** - * Resolves the custom label for a term against one or more sources, checked in - * order (e.g. stage then program). Returns `undefined` when no custom label is - * configured, so the caller keeps its own default term. - * - * For a plural slot: a term with a backend plural field uses that field; a - * singular-only term reuses its singular custom value. - */ export const resolveLabel = ( sources: LabelSource | Array, - key: TermKey, + key: CustomLabelKey, { plural = false }: LabelOptions = {}, ): string | undefined => { - const term: TermDef = CUSTOM_LABEL_FIELDS[key]; + 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); @@ -72,18 +56,18 @@ export const resolveLabel = ( type WithLabels = { customLabels?: CustomLabels } | undefined | null; -export const getProgramLabel = (program: WithLabels, key: TermKey, options?: LabelOptions): string | undefined => +export const getProgramLabel = (program: WithLabels, key: CustomLabelKey, options?: LabelOptions): string | undefined => resolveLabel(program?.customLabels, key, options); export const getStageLabel = ( stage: WithLabels, program: WithLabels, - key: TermKey, + key: CustomLabelKey, options?: LabelOptions, ): string | undefined => resolveLabel([stage?.customLabels, program?.customLabels], key, options); export const getTrackedEntityTypeLabel = ( trackedEntityType: WithLabels, - key: TermKey, + 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 d1b0c77c08..49b34132fe 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts @@ -6,5 +6,5 @@ export { getStageLabel, getTrackedEntityTypeLabel, } from './customLabels'; -export type { TermKey, CustomLabels, LabelOptions } from './customLabels'; +export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; export { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } 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 3d6c0118f5..93c42c78f9 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -2,14 +2,13 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; import { resolveLabel } from './customLabels'; -import type { TermKey, LabelOptions } 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 }; -/** Resolves a program-level custom term; defaults to the currently selected program. */ -export const useProgramLabel = (key: TermKey, { programId, plural }: ProgramOptions = {}): string | undefined => { +export const useProgramLabel = (key: CustomLabelKey, { programId, plural }: ProgramOptions = {}): string | undefined => { const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); const id = programId ?? currentProgramId; return useMemo( @@ -18,8 +17,7 @@ export const useProgramLabel = (key: TermKey, { programId, plural }: ProgramOpti ); }; -/** Resolves a custom term with stage → program fallback; defaults to the current program/stage. */ -export const useStageLabel = (key: TermKey, { programId, stageId, plural }: StageOptions = {}): string | undefined => { +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; @@ -31,9 +29,8 @@ export const useStageLabel = (key: TermKey, { programId, stageId, plural }: Stag }, [pId, sId, key, plural]); }; -/** Resolves a tracked-entity-type-level custom term; defaults to the current tracked entity type. */ export const useTrackedEntityTypeLabel = ( - key: TermKey, + key: CustomLabelKey, { tetId, plural }: TrackedEntityTypeOptions = {}, ): string | undefined => { const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index 59cc2f3f15..627adbd3b3 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -28,4 +28,4 @@ export { useStageLabel, useTrackedEntityTypeLabel, } from './customLabels'; -export type { TermKey, CustomLabels, LabelOptions } 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 4413001843..00e7aca7aa 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -50,4 +50,4 @@ export { useStageLabel, useTrackedEntityTypeLabel, } from './helpers'; -export type { TermKey, CustomLabels, LabelOptions } from './helpers'; +export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; From 1b899d0d0b48eb4dc9770dd57a96a18edb5d98f0 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:04:32 +0200 Subject: [PATCH 08/15] fix: lint long line --- .../capture-core/metaData/helpers/customLabels/useLabel.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 93c42c78f9..c733c2e662 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts @@ -17,7 +17,10 @@ export const useProgramLabel = (key: CustomLabelKey, { programId, plural }: Prog ); }; -export const useStageLabel = (key: CustomLabelKey, { programId, stageId, plural }: StageOptions = {}): string | undefined => { +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; From d8be21ceac5d7445fe91fda01dc8ed989841157e Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:48:55 +0200 Subject: [PATCH 09/15] feat: update terminology and improve label handling in components --- i18n/en.pot | 8 ++++---- .../Pages/Enrollment/MissingMessage.component.tsx | 2 +- .../StageEventHeader/StageEventHeader.component.tsx | 4 ++-- .../components/ReadOnlyBadge/ReadOnlyBadge.tsx | 4 ++-- .../WidgetEnrollmentNote.component.tsx | 4 ++-- .../hooks/useAddEventWithRelationship.ts | 4 ++-- .../metaData/helpers/customLabels/customLabels.ts | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fdd2ea5314..13fe529435 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-06-14T18:14:14.730Z\n" -"PO-Revision-Date: 2026-06-14T18:14:14.731Z\n" +"POT-Creation-Date: 2026-06-17T10:48:57.021Z\n" +"PO-Revision-Date: 2026-06-17T10:48:57.022Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -755,8 +755,8 @@ msgstr "No available program stages" msgid "Invalid {{enrollment}} id {{enrollmentId}}." msgstr "Invalid {{enrollment}} id {{enrollmentId}}." -msgid "Choose this {{enrollment}} to view the dashboard." -msgstr "Choose this {{enrollment}} to view the dashboard." +msgid "Choose an {{enrollment}} to view the dashboard." +msgstr "Choose an {{enrollment}} to view the dashboard." msgid "" "Choose a program to add new or see existing {{enrollments}} for " 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 496a04641b..5e7881fd32 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 @@ -160,7 +160,7 @@ const EnrollmentSelectionMessage = ({ enrollmentId, enrollment }: { enrollmentId enrollmentId, interpolation: { escapeValue: false }, }) : - i18n.t('Choose this {{enrollment}} to view the dashboard.', { enrollment }) + i18n.t('Choose an {{enrollment}} to view the dashboard.', { enrollment }) } ); 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 d65a611dd6..ed02071ad5 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 = useStageLabel('event') ?? i18n.t('event'); const eventsLabel = useStageLabel('event', { plural: true }) ?? i18n.t('events'); - return (<> + return (
      { icon && ( @@ -46,7 +46,7 @@ const StageEventHeaderPlain = ({ icon, title, events, classes }: Props) => { }
      - ); + ); }; export const StageEventHeader = withStyles( diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index e22fb06a7f..8e7ec32efd 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -3,7 +3,7 @@ import { IconInfo16, Tag } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; -import { useStageLabel, useTrackedEntityTypeLabel } from '../../metaData'; +import { useProgramLabel, useStageLabel, useTrackedEntityTypeLabel } from '../../metaData'; const styles = { label: { @@ -95,7 +95,7 @@ const ReadOnlyBadgePlain = ({ programStage: programStageWriteAccess, }; const labels: Labels = { - enrollment: useStageLabel('enrollment') ?? i18n.t('enrollment'), + enrollment: useProgramLabel('enrollment') ?? i18n.t('enrollment'), trackedEntityType: useTrackedEntityTypeLabel('trackedEntityType') ?? i18n.t('tracked entity type'), programStage: useStageLabel('programStage') ?? i18n.t('program stage'), programStages: useStageLabel('programStage', { plural: true }) ?? i18n.t('program stages'), 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 8b61d6dd51..3aac07db2b 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -12,8 +12,8 @@ export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); const { enrollmentId } = useLocationQuery(); const enrollment = useProgramLabel('enrollment') ?? i18n.t('enrollment'); - const notesTitle = useProgramLabel('note', { plural: true }) ?? i18n.t('Notes'); - const notesLower = useProgramLabel('note', { plural: true }) ?? i18n.t('notes'); + const notesTitle = useProgramLabel('note') ?? i18n.t('Notes'); + const notesLower = useProgramLabel('note') ?? i18n.t('notes'); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); const { 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 42f9e886bb..482eba990a 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts @@ -2,7 +2,7 @@ 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 { useStageLabel } from '../../../metaData'; +import { useProgramLabel } from '../../../metaData'; const ReactQueryAppNamespace = 'capture'; @@ -29,7 +29,7 @@ export const useAddEventWithRelationship = ({ }) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); - const event = useStageLabel('event') ?? i18n.t('event'); + const event = useProgramLabel('event') ?? i18n.t('event'); const { show: showSuccess } = useAlert(({ message }) => message, { success: true }); const { show: showAlert } = useAlert(({ message }) => message, { critical: true }); 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..4d769378d2 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -12,7 +12,7 @@ export const CUSTOM_LABEL_FIELDS = { attribute: { field: 'displayTrackedEntityAttributeLabel' }, programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel' }, + trackedEntityType: { field: 'displayName', pluralField: 'displayTrackedEntityTypesLabel' }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; From a624f400c510e77a8f9478781a35d6c9b25ac088 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:16:00 +0200 Subject: [PATCH 10/15] feat: revert useProgramLabel when only singular terminology available --- .../capture-core/components/Notes/Notes.component.tsx | 4 ++-- .../Enrollment/DataEntryEnrollment.component.tsx | 3 +-- .../Pages/ViewEvent/ViewEventPage.component.tsx | 2 +- .../Enrollment/DataEntryEnrollment.component.tsx | 3 +-- .../WidgetEnrollmentNote.component.tsx | 4 ++-- .../WidgetEventNote/WidgetEventNote.component.tsx | 6 +++--- .../components/WidgetNote/NoteSection/NoteSection.tsx | 4 ++-- .../RelatedStagesActions.component.tsx | 4 ++-- .../Breadcrumbs/Breadcrumbs.component.tsx | 3 +-- .../NewTrackedEntityRelationship.component.tsx | 3 +-- .../NewTrackedEntityRelationship.container.tsx | 3 +-- .../hooks/useAddRelationship.ts | 3 +-- .../WidgetTrackedEntityRelationship.component.tsx | 3 +-- .../DeleteRelationship/DeleteRelationship.tsx | 3 +-- .../DeleteRelationship/useDeleteRelationship.ts | 3 +-- .../RelationshipsWidget.component.tsx | 2 +- .../metaData/helpers/customLabels/customLabels.ts | 11 +++++------ 17 files changed, 27 insertions(+), 37 deletions(-) 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 a2971ea291..6409aac2c5 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.tsx +++ b/src/core_modules/capture-core/components/Notes/Notes.component.tsx @@ -9,7 +9,7 @@ import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { withFocusSaver } from 'capture-ui'; import { TextField } from '../FormFields/New'; import { convertClientToList } from '../../converters'; -import { dataElementTypes, useProgramLabel } from '../../metaData'; +import { dataElementTypes } from '../../metaData'; import type { Note } from './notes.types'; const FocusTextField = withFocusSaver()(TextField); @@ -85,7 +85,7 @@ const NotesPlain = ({ const [addIsOpen, setAddIsOpen] = useState(false); const [inputValue, setInputValue] = useState(''); const { fromServerDate } = useTimeZoneConversion(); - const note = useProgramLabel('note') ?? i18n.t('note'); + const note = i18n.t('note'); useEffect(() => { setAddIsOpen(!!propValue); 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 b1a50308c3..392606bfee 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 @@ -1,7 +1,6 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withTheme } from 'capture-core-utils/styles'; -import { useProgramLabel } from '../../../../../../metaData'; import { DATA_ENTRY_ID } from '../../registerTei.const'; import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../DataEntries'; @@ -21,7 +20,7 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; - const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('relationship'); + const relationship = i18n.t('relationship'); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx index cc8defca54..ee6d19de79 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventPage.component.tsx @@ -24,7 +24,7 @@ export const ViewEventPageComponent = ({ isUserInteractionInProgress, eventDetai })); const eventLabel = useStageLabel('event') ?? i18n.t('event'); - const relationshipLabel = useStageLabel('relationship') ?? i18n.t('relationship'); + const relationshipLabel = i18n.t('relationship'); return ( 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 90a95c9bdc..30c2e73b16 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 @@ -1,7 +1,6 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withTheme } from 'capture-core-utils/styles'; -import { useProgramLabel } from '../../../../../../../metaData'; import { DATA_ENTRY_ID } from '../../registerTei.const'; import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; @@ -22,7 +21,7 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; - const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('relationship'); + const relationship = i18n.t('relationship'); const relatedStageActionsOptions = { [relatedStageActions.ENTER_DATA]: { disabled: true, 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 3aac07db2b..fec1c78663 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -12,8 +12,8 @@ export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); const { enrollmentId } = useLocationQuery(); const enrollment = useProgramLabel('enrollment') ?? i18n.t('enrollment'); - const notesTitle = useProgramLabel('note') ?? i18n.t('Notes'); - const notesLower = useProgramLabel('note') ?? i18n.t('notes'); + const notesTitle = i18n.t('Notes'); + const notesLower = i18n.t('notes'); const notes = useSelector(({ enrollmentDomain }: { enrollmentDomain?: { enrollment?: { notes?: Array } } }) => enrollmentDomain?.enrollment?.notes ?? []); const { 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 ca44456043..d389c0c353 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -6,13 +6,13 @@ import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; -import { useProgramLabel, useStageLabel } from '../../metaData'; +import { useStageLabel } from '../../metaData'; export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { const dispatch = useDispatch(); const event = useStageLabel('event') ?? i18n.t('event'); - const notesTitle = useProgramLabel('note', { plural: true }) ?? i18n.t('Notes'); - const notesLower = useProgramLabel('note', { plural: true }) ?? i18n.t('notes'); + const notesTitle = i18n.t('Notes'); + const notesLower = i18n.t('notes'); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); const { 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 10ea10f355..0b230db81a 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import { TextField } from '../../FormFields/New'; import { convertClientToList } from '../../../converters'; -import { dataElementTypes, useProgramLabel } from '../../../metaData'; +import { dataElementTypes } from '../../../metaData'; import type { OwnProps, NoteType } from './NoteSection.types'; const FocusTextField = withFocusSaver()(TextField); @@ -76,7 +76,7 @@ const NoteSectionPlain = ({ const [isEditing, setEditing] = useState(false); const [newNoteValue, setNewNoteValue] = useState(''); const { fromServerDate } = useTimeZoneConversion(); - const note = useProgramLabel('note') ?? i18n.t('note'); + const note = i18n.t('note'); const handleChange = useCallback((value: string) => { setEditing(true); 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 65ae5c0b9b..97d515f9d7 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 @@ -8,7 +8,7 @@ import { useCanAddNewEventToStage } from '../hooks'; import { DataSection } from '../../DataSection'; import { ScheduleInOrgUnit } from '../ScheduleInOrgUnit'; import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollection/helpers'; -import { useStageLabel, useProgramLabel } from '../../../metaData'; +import { useStageLabel } from '../../../metaData'; import type { PlainProps, LinkButtonProps } from './RelatedStagesActions.types'; import { LinkToExisting } from '../LinkToExisting'; import { EnterDataInOrgUnit } from '../EnterDataInOrgUnit/EnterData.component'; @@ -200,7 +200,7 @@ const LinkButton = withStyles(styles)(({ const useRelatedStagesLabels = (stageId?: string, programId?: string) => ({ eventLabel: useStageLabel('event', { stageId, programId }) ?? i18n.t('event'), eventsLabel: useStageLabel('event', { stageId, programId, plural: true }) ?? i18n.t('events'), - relationshipsLabel: useProgramLabel('relationship', { programId, plural: true }) ?? i18n.t('relationships'), + relationshipsLabel: i18n.t('relationships'), }); const RelatedStagesActionsPlain = ({ diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx index 8e96f4ee1e..5e47472876 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx @@ -3,7 +3,6 @@ import i18n from '@dhis2/d2-i18n'; import { colors, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS } from '../wizardSteps.const'; -import { useProgramLabel } from '../../../../../metaData'; import type { PlainProps } from './breadcrumbs.types'; const styles = { @@ -44,7 +43,7 @@ const Slash = withStyles(slashStyles)(({ classes }: WithStyles/); const LinkedEntityMetadataSelectorStep = ({ currentStep, onNavigate, trackedEntityTypeName }: any) => { - const relationship = useProgramLabel('relationship') ?? i18n.t('relationship'); + const relationship = i18n.t('relationship'); const initialText = i18n.t('New {{trackedEntityTypeName}} {{relationship}}', { trackedEntityTypeName: trackedEntityTypeName && trackedEntityTypeName.toLowerCase(), relationship, 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 dc5d929cd5..31aa5648c2 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 @@ -14,7 +14,6 @@ import type { ComponentProps } from './NewTrackedEntityRelationship.types'; import { useAddRelationship } from './hooks/useAddRelationship'; import { TARGET_SIDES } from './common'; import { generateUID } from '../../../../utils/uid/generateUID'; -import { useProgramLabel } from '../../../../metaData'; const styles = { container: { @@ -59,7 +58,7 @@ const NewTrackedEntityRelationshipPlain = ({ teiId, onMutate: () => onSave && onSave(), }); - const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('relationship'); + const relationship = i18n.t('relationship'); const onLinkToTrackedEntityFromSearch = useCallback( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx index 4aea3d32a8..ccb04515c9 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.container.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react'; import { Button, spacersNum } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import i18n from '@dhis2/d2-i18n'; -import { useProgramLabel } from '../../../../metaData'; import { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; import type { ContainerProps } from './NewTrackedEntityRelationship.types'; @@ -30,7 +29,7 @@ const NewTrackedEntityRelationshipPlain = ({ classes, }: ContainerProps & WithStyles) => { const [addWizardVisible, setAddWizardVisible] = useState(false); - const relationship = useProgramLabel('relationship', { programId }) ?? i18n.t('Relationship'); + const relationship = i18n.t('Relationship'); const closeAddWizard = useCallback(() => { setAddWizardVisible(false); 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 9fcf00a193..50d5cb49ad 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 @@ -4,7 +4,6 @@ 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 { useProgramLabel } from '../../../../../metaData'; type Props = { teiId: string; @@ -24,7 +23,7 @@ export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { const queryClient = useQueryClient(); const queryKey: string = useFeature(FEATURES.exportablePayload) ? 'relationships' : 'instances'; const dataEngine = useDataEngine(); - const relationshipLabel = useProgramLabel('relationship') ?? i18n.t('relationship'); + const relationshipLabel = i18n.t('relationship'); const { show: showAlert } = useAlert( i18n.t('An error occurred while adding the {{relationship}}', { relationship: 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 6ec60384f1..5fe5d6f018 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,6 +1,5 @@ import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; -import { useProgramLabel } from '../../../metaData'; import type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; import { RelationshipsWidget } from '../common/RelationshipsWidget'; import { RelationshipSearchEntities, useRelationships } from '../common/useRelationships'; @@ -41,7 +40,7 @@ export const WidgetTrackedEntityRelationship = ({ const isLoading = useMemo(() => isLoadingRelationships || isLoadingTEType, [isLoadingRelationships, isLoadingTEType], ); - const relationshipsLabel = useProgramLabel('relationship', { plural: true, programId }) ?? i18n.t('relationships'); + const relationshipsLabel = i18n.t('relationships'); if (isError) { return ( diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx index ed5cc05da1..bacf592dde 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/DeleteRelationship.tsx @@ -13,7 +13,6 @@ import { } from '@dhis2/ui'; import { IconButton } from 'capture-ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import { useProgramLabel } from '../../../../../metaData'; import type { Props } from './DeleteRelationship.types'; const styles: Readonly = { @@ -29,7 +28,7 @@ export const DeleteRelationshipPlain = ({ classes, }: Props & WithStyles) => { const [isModalOpen, setIsModalOpen] = useState(false); - const relationship = useProgramLabel('relationship') ?? i18n.t('relationship'); + const relationship = i18n.t('relationship'); return ( <> 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 d1cfb929f2..27b0f58278 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 @@ -5,7 +5,6 @@ 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 { useProgramLabel } from '../../../../../metaData'; type Props = { sourceId: string; @@ -29,7 +28,7 @@ export const useDeleteRelationship = ({ sourceId }: Props): { onDeleteRelationsh const dataEngine = useDataEngine(); const queryKey: string = useFeature(FEATURES.exportablePayload) ? 'relationships' : 'instances'; const queryClient = useQueryClient(); - const relationship = useProgramLabel('relationship') ?? i18n.t('relationship'); + const relationship = i18n.t('relationship'); const { show: showError } = useAlert( i18n.t('An error occurred while deleting the {{relationship}}.', { relationship }), { 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 1c1ed73b68..996f36c8da 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 @@ -38,7 +38,7 @@ const RelationshipsWidgetPlain = ({ }: Props & WithStyles) => { const [open, setOpenStatus] = useState(true); const enrollmentLabel = useProgramLabel('enrollment') ?? i18n.t('enrollment'); - const relationshipsLabel = useProgramLabel('relationship', { plural: true }) ?? i18n.t('relationships'); + const relationshipsLabel = i18n.t('relationships'); const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships, readOnly); const { onDeleteRelationship } = useDeleteRelationship({ sourceId }); 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 4d769378d2..8ba70da3d7 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -7,8 +7,6 @@ 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' }, @@ -39,6 +37,9 @@ export const extractCustomLabels = (cached: Record): CustomLabels = type LabelSource = CustomLabels | undefined | null; +const capitalizeFirstChar = (value?: string): string | undefined => + (value ? value.charAt(0).toUpperCase() + value.slice(1) : value); + export const resolveLabel = ( sources: LabelSource | Array, key: CustomLabelKey, @@ -48,10 +49,8 @@ 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.pluralField ? term.pluralField : term.field; + return capitalizeFirstChar(pick(field)); }; type WithLabels = { customLabels?: CustomLabels } | undefined | null; From f490fe1e3cb5d87322ca28cd09f9c289ac186280 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:13:01 +0200 Subject: [PATCH 11/15] feat: refactor custom label fields to use singular and plural properties --- .../helpers/customLabels/customLabels.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) 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 8ba70da3d7..585e8bb617 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -1,16 +1,18 @@ +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' }, - attribute: { field: 'displayTrackedEntityAttributeLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - trackedEntityType: { field: 'displayName', pluralField: 'displayTrackedEntityTypesLabel' }, + enrollment: { singular: 'displayEnrollmentLabel', plural: 'displayEnrollmentsLabel' }, + followUp: { singular: 'displayFollowUpLabel' }, + orgUnit: { singular: 'displayOrgUnitLabel' }, + attribute: { plural: 'displayTrackedEntityAttributeLabel' }, + programStage: { singular: 'displayProgramStageLabel', plural: 'displayProgramStagesLabel' }, + event: { singular: 'displayEventLabel', plural: 'displayEventsLabel' }, + trackedEntityType: { singular: 'displayName', plural: 'displayTrackedEntityTypesLabel' }, } as const satisfies { [key: string]: CustomLabelField }; export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; @@ -20,7 +22,7 @@ export type LabelOptions = { plural?: boolean }; const allFields: Array = 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)), ), ); @@ -37,9 +39,6 @@ export const extractCustomLabels = (cached: Record): CustomLabels = type LabelSource = CustomLabels | undefined | null; -const capitalizeFirstChar = (value?: string): string | undefined => - (value ? value.charAt(0).toUpperCase() + value.slice(1) : value); - export const resolveLabel = ( sources: LabelSource | Array, key: CustomLabelKey, @@ -49,8 +48,9 @@ export const resolveLabel = ( const list = Array.isArray(sources) ? sources : [sources]; const pick = (field?: string) => (field ? list.find(source => source?.[field])?.[field] : undefined); - const field = plural && term.pluralField ? term.pluralField : term.field; - return capitalizeFirstChar(pick(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; From cb5903dad98b8d77ea6168000a7c5c41b37f36f1 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:02:23 +0200 Subject: [PATCH 12/15] fix: (review) enhance custom label extraction with scope support for programs program stages and tracked entity types --- .../WidgetRelatedStages.container.tsx | 1 + .../hooks/useAddEventWithRelationship.ts | 4 +-- .../DeleteActionModal/DeleteActionModal.tsx | 6 ++-- .../Stage/StageDetail/EventRow/EventRow.tsx | 1 + .../helpers/customLabels/customLabels.ts | 35 ++++++++++++++----- .../factory/program/ProgramFactory.ts | 2 +- .../programStage/ProgramStageFactory.ts | 2 +- .../TrackedEntityTypeFactory.ts | 2 +- 8 files changed, 37 insertions(+), 16 deletions(-) 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 11621458ef..515dc24baf 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -64,6 +64,7 @@ export const WidgetRelatedStagesPlain = ({ const { buildRelatedStageEventPayload } = useBuildRelatedStageEventPayload(); const { addEventWithRelationship } = useAddEventWithRelationship({ eventId, + event, onUpdateOrAddEnrollmentEvents, onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, 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 482eba990a..0bef01e20b 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts @@ -2,7 +2,6 @@ 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 { useProgramLabel } from '../../../metaData'; const ReactQueryAppNamespace = 'capture'; @@ -14,6 +13,7 @@ const addEventWithRelationshipMutation = { export const useAddEventWithRelationship = ({ eventId, + event, onUpdateOrAddEnrollmentEvents, onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, @@ -21,6 +21,7 @@ export const useAddEventWithRelationship = ({ setIsLinking, }: { eventId: string; + event: string; onUpdateOrAddEnrollmentEvents: (events: Array) => void; onUpdateEnrollmentEventsSuccess: (events: Array) => void; onUpdateEnrollmentEventsError: (events: Array) => void; @@ -29,7 +30,6 @@ export const useAddEventWithRelationship = ({ }) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); - const event = useProgramLabel('event') ?? i18n.t('event'); const { show: showSuccess } = useAlert(({ message }) => message, { success: true }); const { show: showAlert } = useAlert(({ message }) => message, { critical: true }); 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 9195aa49e7..7f054cc840 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 @@ -7,13 +7,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { errorCreator } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; import { ReactQueryAppNamespace } from '../../../../../../../utils/reactQueryHelpers'; -import { useProgramLabel } from '../../../../../../../metaData'; +import { useStageLabel } from '../../../../../../../metaData'; type Props = { eventId: string; pendingApiResponse: boolean; teiId: string; programId: string; + stageId?: string; enrollmentId: string; onDeleteEvent: (eventId: string) => void; onRollbackDeleteEvent: (eventToRollbackOnFail: ApiEnrollmentEvent) => void; @@ -26,11 +27,12 @@ export const DeleteActionModal = ({ eventId, teiId, programId, + stageId, enrollmentId, onDeleteEvent, onRollbackDeleteEvent, }: Props) => { - const event = useProgramLabel('event', { programId }) ?? i18n.t('event'); + const event = useStageLabel('event', { programId, stageId }) ?? i18n.t('event'); const { show: showError } = useAlert( ({ message }) => message, { diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx index f256556bd9..e023b6955d 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/EventRow.tsx @@ -102,6 +102,7 @@ const EventRowPlain = ({ pendingApiResponse={pendingApiResponse} teiId={teiId} programId={programId} + stageId={programStage?.id} enrollmentId={enrollmentId} onDeleteEvent={onDeleteEvent} onRollbackDeleteEvent={onRollbackDeleteEvent} 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 585e8bb617..696c52fa64 100644 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts @@ -18,18 +18,35 @@ export const CUSTOM_LABEL_FIELDS = { export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; export type CustomLabels = Record; export type LabelOptions = { plural?: boolean }; +export type CustomLabelScope = 'program' | 'programStage' | 'trackedEntityType'; -const allFields: Array = Array.from( - new Set( - Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.singular, term.plural]) - .filter((field): field is string => Boolean(field)), - ), -); +// Each scope lists only the label keys that its cached object can carry. Tracked entity types +// are kept separate because their singular label is the generic `displayName`, which also exists +// on programs and stages — extracting it for them would leak the object's own name. +const KEYS_BY_SCOPE: Record> = { + program: ['enrollment', 'followUp', 'orgUnit', 'attribute', 'programStage', 'event'], + programStage: ['programStage', 'event'], + trackedEntityType: ['trackedEntityType'], +}; + +const fieldsForScope = (scope: CustomLabelScope): Array => + Array.from( + new Set( + KEYS_BY_SCOPE[scope] + .flatMap((key) => { + const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; + return [term.singular, term.plural]; + }) + .filter((field): field is string => Boolean(field)), + ), + ); -export const extractCustomLabels = (cached: Record): CustomLabels => { +export const extractCustomLabels = ( + cached: Record, + scope: CustomLabelScope, +): CustomLabels => { const labels: CustomLabels = {}; - allFields.forEach((field) => { + fieldsForScope(scope).forEach((field) => { if (cached[field]) { labels[field] = cached[field]; } diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts index 1766348564..ad978e61f3 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/program/ProgramFactory.ts @@ -188,7 +188,7 @@ export class ProgramFactory { program.displayFrontPageList = cachedProgram.displayFrontPageList; program.onlyEnrollOnce = cachedProgram.onlyEnrollOnce; program.useFirstStageDuringRegistration = cachedProgram.useFirstStageDuringRegistration; - program.customLabels = extractCustomLabels(cachedProgram); + program.customLabels = extractCustomLabels(cachedProgram, 'program'); return program; } 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..9d0f74e2bf 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 @@ -248,7 +248,7 @@ export class ProgramStageFactory { camelCaseUppercaseString(cachedProgramStage.validationStrategy); }); _stage.icon = buildIcon(cachedProgramStage.style); - _stage.customLabels = extractCustomLabels(cachedProgramStage); + _stage.customLabels = extractCustomLabels(cachedProgramStage, 'programStage'); }); const stageForm = stage.stageForm; 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..364776f107 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,7 @@ export class TrackedEntityTypeFactory { o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.customLabels = extractCustomLabels(cachedType); + o.customLabels = extractCustomLabels(cachedType, 'trackedEntityType'); }); if (cachedType.trackedEntityTypeAttributes) { From cc289c4ff0ba8cc56b715e42c81818cb06d2c1c0 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:38:33 +0200 Subject: [PATCH 13/15] feat: enhance terminology handling when counting --- i18n/en.pot | 57 +++++++++++-------- .../CompleteModal/CompleteModal.component.tsx | 15 +++-- .../Actions/CompleteAction/CompleteAction.tsx | 25 +++++--- .../EnrollmentDeleteModal.tsx | 7 ++- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 8c821265ea..1ead9d74d9 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-06-25T16:14:13.667Z\n" -"PO-Revision-Date: 2026-06-25T16:14:13.668Z\n" +"POT-Creation-Date: 2026-06-27T09:38:35.034Z\n" +"PO-Revision-Date: 2026-06-27T09:38:35.035Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -1429,11 +1429,6 @@ msgstr "" "Would you like to complete the {{enrollment}} 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}}" @@ -2163,15 +2158,31 @@ msgstr "There are currently no active {{enrollments}} in the selection." msgid "All {{enrollments}} are already completed or cancelled." msgstr "All {{enrollments}} 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 {{enrollmentLabel}} 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}} {{enrollmentLabel}} already marked as completed will not be " +"changed." msgid "Mark all {{events}} within {{enrollments}} as complete" msgstr "Mark all {{events}} within {{enrollments}} as complete" @@ -2185,10 +2196,10 @@ msgstr "Error completing {{enrollments}}" msgid "No active {{enrollments}} to complete" msgstr "No active {{enrollments}} 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}} {{enrollmentLabel}}" msgid "An error occurred when completing the {{enrollments}}" msgstr "An error occurred when completing the {{enrollments}}" @@ -2234,10 +2245,10 @@ msgid_plural "Cancelled {{enrollments}} ({{count}})" msgstr[0] "Cancelled {{enrollments}} ({{count}})" msgstr[1] "Cancelled {{enrollments}} ({{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}} {{enrollmentLabel}}" msgid "An error occurred when deleting {{enrollments}}" msgstr "An error occurred when deleting {{enrollments}}" 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 360eb03187..7239b9fb7d 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 @@ -12,6 +12,7 @@ export const CompleteModalComponent = ({ onCompleteEnrollmentAndEvents, }: PlainProps) => { const enrollment = useProgramLabel('enrollment') ?? i18n.t('enrollment'); + const event = useStageLabel('event') ?? i18n.t('event'); const events = useStageLabel('event', { plural: true }) ?? i18n.t('events'); return ( @@ -38,10 +39,11 @@ export const CompleteModalComponent = ({ return (
      • - {i18n.t('{{count}} event in {{programStageName}}', { + {i18n.t('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel: count === 1 ? event : events, + defaultValue: '{{count}} {{eventLabel}} in {{programStageName}}', + defaultValue_plural: '{{count}} {{eventLabel}} in {{programStageName}}', programStageName: name, interpolation: { escapeValue: false }, })} @@ -64,10 +66,11 @@ export const CompleteModalComponent = ({ return (
        • - {i18n.t('{{count}} event in {{programStageName}}', { + {i18n.t('{{count}} {{eventLabel}} in {{programStageName}}', { count, - defaultValue: '{{count}} event in {{programStageName}}', - defaultValue_plural: '{{count}} events in {{programStageName}}', + eventLabel: count === 1 ? event : events, + defaultValue: '{{count}} {{eventLabel}} in {{programStageName}}', + defaultValue_plural: '{{count}} {{eventLabel}} in {{programStageName}}', programStageName: name, interpolation: { escapeValue: 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 9d998522b6..47bcb11300 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 @@ -60,7 +60,9 @@ const CompleteActionPlain = ({ const [modalIsOpen, setModalIsOpen] = useState(false); const [completeEvents, setCompleteEvents] = useState(true); const [openAccordion, setOpenAccordion] = useState(false); + const enrollment = useProgramLabel('enrollment', { programId }) ?? i18n.t('enrollment'); const enrollments = useProgramLabel('enrollment', { plural: true, programId }) ?? i18n.t('enrollments'); + const enrollmentLabelByCount = (n: number) => (n === 1 ? enrollment : enrollments); const events = useProgramLabel('event', { plural: true, programId }) ?? i18n.t('events'); const { completeEnrollments, @@ -152,21 +154,25 @@ const CompleteActionPlain = ({ 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: enrollmentLabelByCount(enrollmentCounts.active), + 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 {{enrollmentLabel}} in your selection.', }) } {' '} {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: enrollmentLabelByCount(enrollmentCounts.completed), + defaultValue: '{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', + // eslint-disable-next-line max-len + defaultValue_plural: '{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', }) } @@ -229,10 +235,11 @@ const CompleteActionPlain = ({ loading={isCompleting} dataTest={'bulk-complete-enrollments-confirm-button'} > - {i18n.t('Complete {{count}} enrollment', { + {i18n.t('Complete {{count}} {{enrollmentLabel}}', { count: enrollmentCounts.active, - defaultValue: 'Complete {{count}} enrollment', - defaultValue_plural: 'Complete {{count}} enrollments', + enrollmentLabel: enrollmentLabelByCount(enrollmentCounts.active), + defaultValue: 'Complete {{count}} {{enrollmentLabel}}', + defaultValue_plural: 'Complete {{count}} {{enrollmentLabel}}', })} 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 c90b6aa1af..79cd6ceb52 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 @@ -185,10 +185,11 @@ const EnrollmentDeleteModalPlain = ({ onClick={deleteEnrollments} disabled={isDeletingEnrollments || numberOfEnrollmentsToDelete === 0} > - {i18n.t('Delete {{count}} enrollment', { + {i18n.t('Delete {{count}} {{enrollmentLabel}}', { count: numberOfEnrollmentsToDelete, - defaultValue: 'Delete {{count}} enrollment', - defaultValue_plural: 'Delete {{count}} enrollments', + enrollmentLabel: numberOfEnrollmentsToDelete === 1 ? enrollment : enrollments, + defaultValue: 'Delete {{count}} {{enrollmentLabel}}', + defaultValue_plural: 'Delete {{count}} {{enrollmentLabel}}', })} From a404ae0466dfbd24a434fea951def8ad0517bc9d Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:37:47 +0200 Subject: [PATCH 14/15] fix: stop dispalying error onblur --- i18n/en.pot | 12 ++++++++++-- .../Contents/withMinCharsToSearchValidation.tsx | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0f1121d0c5..b09f4b02d0 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-03T08:30:06.030Z\n" -"PO-Revision-Date: 2026-07-03T08:30:06.031Z\n" +"POT-Creation-Date: 2026-07-20T10:37:48.509Z\n" +"PO-Revision-Date: 2026-07-20T10:37:48.509Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -2542,3 +2542,11 @@ msgstr "" "This app is currently running in compatibility mode due to browser " "restrictions. For better performance, use another browser or exit private " "mode if this is currently in use." + +msgctxt "Application title" +msgid "__MANIFEST_APP_TITLE" +msgstr "Capture" + +msgctxt "Title for shortcut used by command palette" +msgid "__MANIFEST_SHORTCUT_Search TEI" +msgstr "Search TEI" diff --git a/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx b/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx index a8da9a0b9d..2753dc56ba 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx +++ b/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx @@ -85,9 +85,11 @@ export const withMinCharsToSearchValidation = () => (InnerComponent: React.Compo const WithMinCharsToSearchValidation = (props: any) => { const { filterTypeRef, minCharactersToSearch, handleCommitValue, classes, type, ...rest } = props; const committedValueRef = useRef(undefined); + const errorsVisibleRef = useRef(false); const [committedValue, setCommittedValue] = useState(undefined); const showValidationErrors = useCallback(() => { + errorsVisibleRef.current = true; setCommittedValue(committedValueRef.current); }, []); @@ -110,7 +112,7 @@ export const withMinCharsToSearchValidation = () => (InnerComponent: React.Compo const wrappedHandleCommitValue = useCallback( (value?: Value, isBlur?: boolean) => { committedValueRef.current = value; - if (isBlur) { + if (errorsVisibleRef.current) { setCommittedValue(value); } handleCommitValue?.(value, isBlur); From bbb057899ee64ae2810ea218d9cb64b68bb34833 Mon Sep 17 00:00:00 2001 From: henrikmv <110386561+henrikmv@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:02:14 +0200 Subject: [PATCH 15/15] Revert "fix: stop dispalying error onblur" --- i18n/en.pot | 4 ++-- .../Filters/Contents/withMinCharsToSearchValidation.tsx | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index b09f4b02d0..676b3a7a25 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-20T10:37:48.509Z\n" -"PO-Revision-Date: 2026-07-20T10:37:48.509Z\n" +"POT-Creation-Date: 2026-07-20T11:02:16.087Z\n" +"PO-Revision-Date: 2026-07-20T11:02:16.087Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." diff --git a/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx b/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx index 2753dc56ba..a8da9a0b9d 100644 --- a/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx +++ b/src/core_modules/capture-core/components/ListView/Filters/Contents/withMinCharsToSearchValidation.tsx @@ -85,11 +85,9 @@ export const withMinCharsToSearchValidation = () => (InnerComponent: React.Compo const WithMinCharsToSearchValidation = (props: any) => { const { filterTypeRef, minCharactersToSearch, handleCommitValue, classes, type, ...rest } = props; const committedValueRef = useRef(undefined); - const errorsVisibleRef = useRef(false); const [committedValue, setCommittedValue] = useState(undefined); const showValidationErrors = useCallback(() => { - errorsVisibleRef.current = true; setCommittedValue(committedValueRef.current); }, []); @@ -112,7 +110,7 @@ export const withMinCharsToSearchValidation = () => (InnerComponent: React.Compo const wrappedHandleCommitValue = useCallback( (value?: Value, isBlur?: boolean) => { committedValueRef.current = value; - if (errorsVisibleRef.current) { + if (isBlur) { setCommittedValue(value); } handleCommitValue?.(value, isBlur);