diff --git a/i18n/en.pot b/i18n/en.pot index 424ac8f969..09ae7e13eb 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-31T05:02:39.077Z\n" -"PO-Revision-Date: 2026-08-31T05:02:39.077Z\n" +"POT-Creation-Date: 2026-09-07T10:37:51.439Z\n" +"PO-Revision-Date: 2026-09-07T10:37:51.439Z\n" msgid "The application could not be loaded." msgstr "The application could not be loaded." @@ -42,35 +42,35 @@ msgstr "Bulk data entry" msgid "Program overview" msgstr "Program overview" -msgid "Active enrollments" -msgstr "Active enrollments" +msgid "Active {{enrollmentsLabel}}" +msgstr "Active {{enrollmentsLabel}}" -msgid "Completed enrollments" -msgstr "Completed enrollments" +msgid "Completed {{enrollmentsLabel}}" +msgstr "Completed {{enrollmentsLabel}}" -msgid "Cancelled enrollments" -msgstr "Cancelled enrollments" +msgid "Cancelled {{enrollmentsLabel}}" +msgstr "Cancelled {{enrollmentsLabel}}" msgid "Search" msgstr "Search" -msgid "Enrollment dashboard" -msgstr "Enrollment dashboard" +msgid "{{enrollmentLabel}} dashboard" +msgstr "{{enrollmentLabel}} dashboard" -msgid "View event" -msgstr "View event" +msgid "View {{eventLabel}}" +msgstr "View {{eventLabel}}" -msgid "Edit event" -msgstr "Edit event" +msgid "Edit {{eventLabel}}" +msgstr "Edit {{eventLabel}}" -msgid "New event" -msgstr "New event" +msgid "New {{eventLabel}}" +msgstr "New {{eventLabel}}" msgid "Loading..." msgstr "Loading..." -msgid "Event list" -msgstr "Event list" +msgid "{{eventLabel}} list" +msgstr "{{eventLabel}} list" msgid "More" msgstr "More" @@ -81,8 +81,8 @@ msgstr "View {{programName}} dashboard" msgid "View dashboard" msgstr "View dashboard" -msgid "View active enrollment" -msgstr "View active enrollment" +msgid "View active {{enrollmentLabel}}" +msgstr "View active {{enrollmentLabel}}" msgid "Re-enroll in {{programName}}" msgstr "Re-enroll in {{programName}}" @@ -96,11 +96,11 @@ msgstr "Enrolled" msgid "Previously enrolled" msgstr "Previously enrolled" -msgid "Organisation unit" -msgstr "Organisation unit" +msgid "{{orgUnitLabel}}" +msgstr "{{orgUnitLabel}}" -msgid "Date of enrollment" -msgstr "Date of enrollment" +msgid "Date of {{enrollmentLabel}}" +msgstr "Date of {{enrollmentLabel}}" msgid "Last updated" msgstr "Last updated" @@ -141,11 +141,8 @@ msgstr "Area" msgid "Coordinate" msgstr "Coordinate" -msgid "Enrollment" -msgstr "Enrollment" - -msgid "Complete event" -msgstr "Complete event" +msgid "Complete {{eventLabel}}" +msgstr "Complete {{eventLabel}}" msgid "" "The date entered belongs to an expired period. Enter a date after " @@ -175,8 +172,8 @@ msgstr "Please select {{categoryName}}" msgid "A date in the future is not allowed" msgstr "A date in the future is not allowed" -msgid "Saving a new enrollment in {{programName}} in {{orgUnitName}}." -msgstr "Saving a new enrollment in {{programName}} in {{orgUnitName}}." +msgid "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." +msgstr "Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}." msgid "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." msgstr "Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}." @@ -199,8 +196,11 @@ msgstr "Assigned user" msgid "Search for user" msgstr "Search for user" -msgid "Notes" -msgstr "Notes" +msgid "Saving to {{programName}} in {{orgUnitName}}" +msgstr "Saving to {{programName}} in {{orgUnitName}}" + +msgid "Saving to {{programName}}" +msgstr "Saving to {{programName}}" msgid "Basic info" msgstr "Basic info" @@ -208,18 +208,9 @@ msgstr "Basic info" msgid "Status" msgstr "Status" -msgid "Relationships" -msgstr "Relationships" - msgid "Assignee" msgstr "Assignee" -msgid "Saving to {{programName}} in {{orgUnitName}}" -msgstr "Saving to {{programName}} in {{orgUnitName}}" - -msgid "Saving to {{programName}}" -msgstr "Saving to {{programName}}" - msgid "" "This is not an event program or the metadata is corrupt. See log for " "details." @@ -227,15 +218,15 @@ msgstr "" "This is not an event program or the metadata is corrupt. See log for " "details." -msgid "This event" -msgstr "This event" +msgid "This {{eventLabel}}" +msgstr "This {{eventLabel}}" msgid "" -"Relationship of type {{relationshipTypeName}} to {{entityName}} already " -"exists" +"{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} " +"already exists" msgstr "" -"Relationship of type {{relationshipTypeName}} to {{entityName}} already " -"exists" +"{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} " +"already exists" msgid "Active" msgstr "Active" @@ -243,11 +234,11 @@ msgstr "Active" msgid "Completed" msgstr "Completed" -msgid "Please add or cancel the note before saving the event" -msgstr "Please add or cancel the note before saving the event" +msgid "Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}" +msgstr "Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}" -msgid "Please provide an valid organisation unit" -msgstr "Please provide an valid organisation unit" +msgid "Please provide a valid {{orgUnitLabel}}" +msgstr "Please provide a valid {{orgUnitLabel}}" msgid "Save and add another" msgstr "Save and add another" @@ -276,32 +267,32 @@ 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 "{{count}} {{eventLabel}} added" +msgid_plural "{{count}} {{eventLabel}} added" +msgstr[0] "{{count}} {{eventLabel}} added" +msgstr[1] "{{count}} {{eventsLabel}} added" -msgid "No events added" -msgstr "No events added" +msgid "No {{eventsLabel}} added" +msgstr "No {{eventsLabel}} added" -msgid "New event relationship" -msgstr "New event relationship" +msgid "Adding {{relationshipLabel}} to {{eventLabel}}." +msgstr "Adding {{relationshipLabel}} to {{eventLabel}}." -msgid "Adding relationship to event." -msgstr "Adding relationship to event." +msgid "Go back to {{eventLabel}} without saving {{relationshipLabel}}" +msgstr "Go back to {{eventLabel}} without saving {{relationshipLabel}}" -msgid "Go back to event without saving relationship" -msgstr "Go back to event without saving relationship" +msgid "New {{eventLabel}} {{relationshipLabel}}" +msgstr "New {{eventLabel}} {{relationshipLabel}}" msgid "Discard unsaved changes?" msgstr "Discard unsaved changes?" msgid "" "Leaving this page will discard the selections you made for a new " -"relationship" +"{{relationshipLabel}}" msgstr "" "Leaving this page will discard the selections you made for a new " -"relationship" +"{{relationshipLabel}}" msgid "Yes, discard changes" msgstr "Yes, discard changes" @@ -309,8 +300,8 @@ msgstr "Yes, discard changes" msgid "No, cancel" msgstr "No, cancel" -msgid "You don't have access to create an event in the current selections" -msgstr "You don't have access to create an event in the current selections" +msgid "You don't have access to create an {{eventLabel}} in the current selections" +msgstr "You don't have access to create an {{eventLabel}} in the current selections" msgid "Saving a {{trackedEntityName}}" msgstr "Saving a {{trackedEntityName}}" @@ -318,9 +309,6 @@ msgstr "Saving a {{trackedEntityName}}" msgid "without" msgstr "without" -msgid "enrollment" -msgstr "enrollment" - msgid "in" msgstr "in" @@ -333,25 +321,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 " +"{{eventsLabel}} as well?" +msgstr "" +"Would you like to complete the {{enrollmentLabel}} and all active " +"{{eventsLabel}} 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}} {{eventsLabel}} in {{programStageName}}" -msgid "Yes, complete enrollment and events" -msgstr "Yes, complete enrollment and events" +msgid "Yes, complete {{enrollmentLabel}} and {{eventsLabel}}" +msgstr "Yes, complete {{enrollmentLabel}} and {{eventsLabel}}" -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" @@ -388,20 +380,20 @@ msgstr "Form foundation missing. See log for details" msgid "validation failed" msgstr "validation failed" -msgid "No feedback for this event yet" -msgstr "No feedback for this event yet" +msgid "No feedback for this {{eventLabel}} yet" +msgstr "No feedback for this {{eventLabel}} yet" -msgid "No indicator output for this event yet" -msgstr "No indicator output for this event yet" +msgid "No indicator output for this {{eventLabel}} yet" +msgstr "No indicator output for this {{eventLabel}} yet" -msgid "Generate new event" -msgstr "Generate new event" +msgid "Generate new {{eventLabel}}" +msgstr "Generate new {{eventLabel}}" -msgid "Do you want to create another event?" -msgstr "Do you want to create another event?" +msgid "Do you want to create another {{eventLabel}}?" +msgstr "Do you want to create another {{eventLabel}}?" -msgid "Yes, create new event" -msgstr "Yes, create new event" +msgid "Yes, create new {{eventLabel}}" +msgstr "Yes, create new {{eventLabel}}" msgid "Back to form" msgstr "Back to form" @@ -424,24 +416,11 @@ msgstr "Some operations are still running. Please wait." msgid "Operations running" msgstr "Operations running" -msgid "No feedback for this enrollment yet" -msgstr "No feedback for this enrollment yet" - -msgid "No indicator output for this enrollment yet" -msgstr "No indicator output for this enrollment yet" - -msgid "" -"This event has unsaved changes. Leaving this page without saving will lose " -"these changes. Are you sure you want to discard unsaved changes?" -msgstr "" -"This event has unsaved changes. Leaving this page without saving will lose " -"these changes. Are you sure you want to discard unsaved changes?" - -msgid "No events to display" -msgstr "No events to display" +msgid "No feedback for this {{enrollmentLabel}} yet" +msgstr "No feedback for this {{enrollmentLabel}} yet" -msgid "Data for offline list not present" -msgstr "Data for offline list not present" +msgid "No indicator output for this {{enrollmentLabel}} yet" +msgstr "No indicator output for this {{enrollmentLabel}} yet" msgid "Close" msgstr "Close" @@ -593,8 +572,8 @@ msgstr "Type to filter options" msgid "No match found" msgstr "No match found" -msgid "Search for an organisation unit" -msgstr "Search for an organisation unit" +msgid "Search for an {{orgUnitLabel}}" +msgstr "Search for an {{orgUnitLabel}}" msgid "Clear" msgstr "Clear" @@ -617,6 +596,9 @@ msgstr "start typing to search" msgid "suggestions could not be retrieved" msgstr "suggestions could not be retrieved" +msgid "Data for offline list not present" +msgstr "Data for offline list not present" + msgid "No items to display" msgstr "No items to display" @@ -688,26 +670,26 @@ msgstr "before or equal to" msgid "More filters" msgstr "More filters" -msgid "Stage filters" -msgstr "Stage filters" +msgid "{{programStageLabel}} filters" +msgstr "{{programStageLabel}} filters" msgid "Rows per page" msgstr "Rows per page" -msgid "Could not get organisation unit" -msgstr "Could not get organisation unit" +msgid "Could not get {{orgUnitLabel}}" +msgstr "Could not get {{orgUnitLabel}}" msgid "Program doesn't exist" msgstr "Program doesn't exist" -msgid "Selected program is invalid for selected organisation unit" -msgstr "Selected program is invalid for selected organisation unit" +msgid "Selected program is invalid for selected {{orgUnitLabel}}" +msgstr "Selected program is invalid for selected {{orgUnitLabel}}" -msgid "Add note" -msgstr "Add note" +msgid "Add {{noteLabel}}" +msgstr "Add {{noteLabel}}" -msgid "Write note" -msgstr "Write note" +msgid "Write {{noteLabel}}" +msgstr "Write {{noteLabel}}" msgid "{{fieldName}} was blanked out and hidden by your last action" msgstr "{{fieldName}} was blanked out and hidden by your last action" @@ -721,43 +703,36 @@ msgstr "Close the notice" msgid "Quick actions" msgstr "Quick actions" -msgid "Schedule an event" -msgstr "Schedule an event" +msgid "Schedule an {{eventLabel}}" +msgstr "Schedule an {{eventLabel}}" msgid "Make referral" msgstr "Make referral" -msgid "No available program stages" -msgstr "No available program stages" +msgid "No available {{programStagesLabel}}" +msgstr "No available {{programStagesLabel}}" -msgid "Invalid enrollment id {{enrollmentId}}." -msgstr "Invalid enrollment id {{enrollmentId}}." +msgid "Invalid {{enrollmentLabel}} id {{enrollmentId}}." +msgstr "Invalid {{enrollmentLabel}} id {{enrollmentId}}." -msgid "Choose an enrollment to view the dashboard." -msgstr "Choose an enrollment to view the dashboard." +msgid "Choose an {{enrollmentLabel}} to view the dashboard." +msgstr "Choose an {{enrollmentLabel}} to view the dashboard." msgid "" -"Choose a program to add new or see existing enrollments for " +"Choose a program to add new or see existing {{enrollmentsLabel}} for " "{{teiDisplayName}}" msgstr "" -"Choose a program to add new or see existing enrollments for " +"Choose a program to add new or see existing {{enrollmentsLabel}} for " "{{teiDisplayName}}" msgid "{{programName}} has categories. Choose all categories to view dashboard." msgstr "{{programName}} has categories. Choose all categories to view dashboard." -msgid "There are no active enrollments." -msgstr "There are no active enrollments." - -msgid "Add new enrollment for {{teiDisplayName}} in this program." -msgstr "Add new enrollment for {{teiDisplayName}} in this program." +msgid "There are no active {{enrollmentsLabel}}." +msgstr "There are no active {{enrollmentsLabel}}." -msgid "" -"You do not have permissions to access to this program, registering unit or " -"record, contact your administrator for more information." -msgstr "" -"You do not have permissions to access to this program, registering unit or " -"record, contact your administrator for more information." +msgid "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." +msgstr "Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program." msgid "{{teiDisplayName}} is not enrolled in this program." msgstr "{{teiDisplayName}} is not enrolled in this program." @@ -767,25 +742,27 @@ msgstr "Enroll {{teiDisplayName}} in this program." msgid "" "{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " -"{{programName}}. Choose another program that allows {{tetName}} enrollment. " +"{{programName}}. Choose another program that allows {{tetName}} " +"{{enrollmentLabel}}. " msgstr "" "{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the " -"{{programName}}. Choose another program that allows {{tetName}} enrollment. " +"{{programName}}. Choose another program that allows {{tetName}} " +"{{enrollmentLabel}}. " msgid "Enroll a new {{selectedTetName}} in this program." msgstr "Enroll a new {{selectedTetName}} in this program." -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 {{enrollmentsLabel}}." +msgstr "{{programName}} is an event program and does not have {{enrollmentsLabel}}." -msgid "Create a new event in this program." -msgstr "Create a new event in this program." +msgid "Create a new {{eventLabel}} in this program." +msgstr "Create a new {{eventLabel}} in this program." msgid "View working list in this program." msgstr "View working list in this program." -msgid "Enrollment with id \"{{enrollmentId}}\" does not exist" -msgstr "Enrollment with id \"{{enrollmentId}}\" does not exist" +msgid "{{enrollmentLabel}} with id \"{{enrollmentId}}\" does not exist" +msgstr "{{enrollmentLabel}} with id \"{{enrollmentId}}\" does not exist" msgid "Tracked entity instance with id \"{{teiId}}\" does not exist" msgstr "Tracked entity instance with id \"{{teiId}}\" does not exist" @@ -793,8 +770,12 @@ msgstr "Tracked entity instance with id \"{{teiId}}\" does not exist" msgid "Program with id \"{{programId}}\" does not exist" msgstr "Program with id \"{{programId}}\" does not exist" -msgid "An error occurred while fetching enrollments. Please enter a valid url." -msgstr "An error occurred while fetching enrollments. Please enter a valid url." +msgid "" +"An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid " +"url." +msgstr "" +"An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid " +"url." msgid "An error has occurred" msgstr "An error has occurred" @@ -808,11 +789,11 @@ 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 "{{programStageLabel}} is invalid" +msgstr "{{programStageLabel}} is invalid" -msgid "Stage not found" -msgstr "Stage not found" +msgid "{{programStageLabel}} not found" +msgstr "{{programStageLabel}} not found" msgid "Report" msgstr "Report" @@ -823,26 +804,23 @@ 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 }} {{eventsLabel}}" +msgstr "You can't add any more {{ programStageName }} {{eventsLabel}}" msgid "Cancel without saving" msgstr "Cancel without saving" -msgid "Choose a stage for a new event" -msgstr "Choose a stage for a new event" +msgid "Choose a {{programStageLabel}} for a new {{eventLabel}}" +msgstr "Choose a {{programStageLabel}} for a new {{eventLabel}}" -msgid "Program Stages could not be loaded" -msgstr "Program Stages could not be loaded" +msgid "{{programStagesLabel}} could not be loaded" +msgstr "{{programStagesLabel}} could not be loaded" -msgid "Stage" -msgstr "Stage" +msgid "{{programStageLabel}}" +msgstr "{{programStageLabel}}" -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 "The category option is not valid for the selected {{orgUnitLabel}}." +msgstr "The category option is not valid for the selected {{orgUnitLabel}}." msgid "Please select a valid combination." msgstr "Please select a valid combination." @@ -854,10 +832,10 @@ msgid "Report data" msgstr "Report data" msgid "" -"Choose a program and organisation unit to see existing data and create new " +"Choose a program and {{orgUnitLabel}} to see existing data and create new " "records." msgstr "" -"Choose a program and organisation unit to see existing data and create new " +"Choose a program and {{orgUnitLabel}} to see existing data and create new " "records." msgid "Click 'Search'. For program-specific results, choose a program first." @@ -869,17 +847,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 an {{orgUnitLabel}}" +msgstr "Please select an {{orgUnitLabel}}" -msgid "See working list without organisation unit" -msgstr "See working list without organisation unit" +msgid "See working list without {{orgUnitLabel}}" +msgstr "See working list without {{orgUnitLabel}}" msgid "Search for a {{trackedEntityName}}" msgstr "Search for a {{trackedEntityName}}" -msgid "Registered events" -msgstr "Registered events" +msgid "Registered {{eventsLabel}}" +msgstr "Registered {{eventsLabel}}" msgid "" "You don't have access to create a {{trackedEntityName}} in the current " @@ -888,8 +866,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 an {{orgUnitLabel}} to start reporting" +msgstr "Choose an {{orgUnitLabel}} to start reporting" msgid "Choose the {{missingCategories}} to start reporting" msgstr "Choose the {{missingCategories}} to start reporting" @@ -903,8 +881,8 @@ msgstr "New" msgid "You can also choose a program from the top bar and create in that program" msgstr "You can also choose a program from the top bar and create in that program" -msgid "New Enrollment in program{{escape}} {{programName}}" -msgstr "New Enrollment in program{{escape}} {{programName}}" +msgid "New {{enrollmentLabel}} in program{{escape}} {{programName}}" +msgstr "New {{enrollmentLabel}} in program{{escape}} {{programName}}" msgid "Save {{trackedEntityTypeName}}" msgstr "Save {{trackedEntityTypeName}}" @@ -912,8 +890,8 @@ 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 "Enter details now is not available when creating a {{relationshipLabel}}" +msgstr "Enter details now is not available when creating a {{relationshipLabel}}" msgid "Save new {{trackedEntityTypeName}} and link" msgstr "Save new {{trackedEntityTypeName}} and link" @@ -933,9 +911,6 @@ msgstr "Show all" msgid "Program" msgstr "Program" -msgid "Organisation Unit" -msgstr "Organisation Unit" - msgid "Registration" msgstr "Registration" @@ -963,45 +938,57 @@ msgstr "Register" msgid "Back" msgstr "Back" -msgid "events" -msgstr "events" - -msgid "event" -msgstr "event" +msgid "{{count}} {{eventLabel}}" +msgid_plural "{{count}} {{eventLabel}}" +msgstr[0] "{{count}} {{eventLabel}}" +msgstr[1] "{{count}} {{eventsLabel}}" msgid "View changelog" msgstr "View changelog" -msgid "Event details" -msgstr "Event details" +msgid "{{eventLabel}} details" +msgstr "{{eventLabel}} details" msgid "" "Leaving this page will discard any selections you made for a new " -"relationship" +"{{relationshipLabel}}" msgstr "" "Leaving this page will discard any selections you made for a new " -"relationship" +"{{relationshipLabel}}" msgid "Errors" msgstr "Errors" -msgid "This event doesn't have any notes" -msgstr "This event doesn't have any notes" +msgid "{{notesLabel}}" +msgstr "{{notesLabel}}" + +msgid "This {{eventLabel}} doesn't have any {{notesLabel}}" +msgstr "This {{eventLabel}} doesn't have any {{notesLabel}}" -msgid "This event doesn't have any relationships" -msgstr "This event doesn't have any relationships" +msgid "{{relationshipsLabel}}" +msgstr "{{relationshipsLabel}}" + +msgid "This {{eventLabel}} doesn't have any {{relationshipsLabel}}" +msgstr "This {{eventLabel}} doesn't have any {{relationshipsLabel}}" msgid "Warnings" msgstr "Warnings" -msgid "Event could not be loaded. Are you sure it exists?" -msgstr "Event could not be loaded. Are you sure it exists?" +msgid "{{eventLabel}} could not be loaded. Are you sure it exists?" +msgstr "{{eventLabel}} could not be loaded. Are you sure it exists?" + +msgid "{{eventLabel}} could not be loaded" +msgstr "{{eventLabel}} could not be loaded" -msgid "Event could not be loaded" -msgstr "Event could not be loaded" +msgid "" +"Could not load the requested data. It may not exist or you may not have " +"access." +msgstr "" +"Could not load the requested data. It may not exist or you may not have " +"access." -msgid "Organisation unit could not be loaded" -msgstr "Organisation unit could not be loaded" +msgid "{{orgUnitLabel}} could not be loaded" +msgstr "{{orgUnitLabel}} could not be loaded" msgid "tracked entity instance" msgstr "tracked entity instance" @@ -1012,11 +999,11 @@ msgstr "All accessible" msgid "Selected" msgstr "Selected" -msgid "Please select an organisation unit." -msgstr "Please select an organisation unit." +msgid "Please select an {{orgUnitLabel}}." +msgstr "Please select an {{orgUnitLabel}}." -msgid "Organisation unit scope" -msgstr "Organisation unit scope" +msgid "{{orgUnitLabel}} scope" +msgstr "{{orgUnitLabel}} scope" msgid "Selected program" msgstr "Selected program" @@ -1024,17 +1011,17 @@ msgstr "Selected program" msgid "Search {{uniqueAttrName}}" msgstr "Search {{uniqueAttrName}}" -msgid "Search by attributes" -msgstr "Search by attributes" +msgid "Search by {{attributesLabel}}" +msgstr "Search by {{attributesLabel}}" + +msgid "Fill in at least {{count}} {{attributeLabel}} to search" +msgid_plural "Fill in at least {{count}} {{attributeLabel}} to search" +msgstr[0] "Fill in at least {{count}} {{attributeLabel}} to search" +msgstr[1] "Fill in at least {{count}} attributes to search" msgid "Search {{attributeName}}" msgstr "Search {{attributeName}}" -msgid "Fill in at least {{count}} attribute to search" -msgid_plural "Fill in at least {{count}} attribute to search" -msgstr[0] "Fill in at least {{count}} attribute to search" -msgstr[1] "Fill in at least {{count}} attributes to search" - msgid "Missing search criteria" msgstr "Missing search criteria" @@ -1050,8 +1037,8 @@ msgstr "Search form is missing. See log for details" msgid "Could not retrieve metadata. Please try again later." msgstr "Could not retrieve metadata. Please try again later." -msgid "The enrollment event data could not be found" -msgstr "The enrollment event data could not be found" +msgid "The {{enrollmentLabel}} {{eventLabel}} data could not be found" +msgstr "The {{enrollmentLabel}} {{eventLabel}} data could not be found" msgid "Loading" msgstr "Loading" @@ -1074,8 +1061,8 @@ msgstr "Possible duplicates found" msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" -msgid "You only have view access to this enrollment" -msgstr "You only have view access to this enrollment" +msgid "You only have view access to this {{enrollmentLabel}}" +msgstr "You only have view access to this {{enrollmentLabel}}" msgid "You only have view access to this program" msgstr "You only have view access to this program" @@ -1086,17 +1073,17 @@ msgstr "You only have view access to this {{trackedEntityName}}" msgid "You only have view access to this tracked entity type" msgstr "You only have view access to this tracked entity type" -msgid "You only have view access to these program stages" -msgstr "You only have view access to these program stages" +msgid "You only have view access to these {{programStagesLabel}}" +msgstr "You only have view access to these {{programStagesLabel}}" -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 {{programStageLabel}}" +msgstr "You only have view access to this {{programStageLabel}}" -msgid "This event is outside the editing period" -msgstr "This event is outside the editing period" +msgid "This {{eventLabel}} is outside the editing period" +msgstr "This {{eventLabel}} is outside the editing period" -msgid "This event has been completed" -msgstr "This event has been completed" +msgid "This {{eventLabel}} has been completed" +msgstr "This {{eventLabel}} has been completed" msgid "This {{trackedEntityName}} is deactivated" msgstr "This {{trackedEntityName}} is deactivated" @@ -1110,20 +1097,20 @@ msgstr "View only - {{message}}" msgid "View only" msgstr "View only" -msgid "Add relationship" -msgstr "Add relationship" +msgid "Add {{relationshipLabel}}" +msgstr "Add {{relationshipLabel}}" msgid "No results found for " msgstr "No results found for " -msgid "Choose an organisation unit in the form below" -msgstr "Choose an organisation unit in the form below" +msgid "Choose an {{orgUnitLabel}} in the form below" +msgstr "Choose an {{orgUnitLabel}} in the form below" msgid "None selected" msgstr "None selected" -msgid "Choose an organisation unit" -msgstr "Choose an organisation unit" +msgid "Choose an {{orgUnitLabel}}" +msgstr "Choose an {{orgUnitLabel}}" msgid "Choose a {{categoryName}}" msgstr "Choose a {{categoryName}}" @@ -1140,8 +1127,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 {{orgUnitLabel}}" +msgstr "Some programs are being filtered by the chosen {{orgUnitLabel}}" msgid "Show all programs" msgstr "Show all programs" @@ -1165,8 +1152,8 @@ msgstr "" msgid "Choose a type to start searching" msgstr "Choose a type to start searching" -msgid "{{trackedEntityName}} has no searchable attributes" -msgstr "{{trackedEntityName}} has no searchable attributes" +msgid "{{trackedEntityName}} has no searchable {{attributesLabel}}" +msgstr "{{trackedEntityName}} has no searchable {{attributesLabel}}" msgid "" "Try selecting a different tracked entity type, or try searching in a " @@ -1190,8 +1177,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 {{attributesLabel}} for {{trackedEntityName}}" +msgstr "No searchable {{attributesLabel}} for {{trackedEntityName}}" msgid "Search in all programs" msgstr "Search in all programs" @@ -1212,8 +1199,8 @@ msgstr "" "Fill in at least {{minAttributesRequiredToSearch}} of these fields to " "search{{escape}} {{searchableAttributes}}" -msgid "Fill in this field to search{{escape}} {{searchableAttributes}}" -msgstr "Fill in this field to search{{escape}} {{searchableAttributes}}" +msgid "Fill in this {{attributeLabel}} to search{{escape}} {{searchableAttributes}}" +msgstr "Fill in this {{attributeLabel}} to search{{escape}} {{searchableAttributes}}" msgid "" "You can change your search terms and search again to find what you are " @@ -1242,11 +1229,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 {{attributesLabel}} 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 {{attributesLabel}} to " +"narrow down the results." msgid "Cannot search in all programs" msgstr "Cannot search in all programs" @@ -1276,8 +1263,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 {{eventLabel}}" +msgstr "Create new {{eventLabel}}" msgid "Search for a {{trackedEntityName}} in {{programName}}" msgstr "Search for a {{trackedEntityName}} in {{programName}}" @@ -1300,37 +1287,37 @@ 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 {{eventLabel}}" +msgstr "No one is assigned to this {{eventLabel}}" msgid "Assign" msgstr "Assign" +msgid "Check for {{enrollmentsLabel}}" +msgstr "Check for {{enrollmentsLabel}}" + msgid "This program is protected" msgstr "This program is protected" -msgid "Reason to check for enrollments" -msgstr "Reason to check for enrollments" - msgid "" -"Describe the reason you are checking for enrollments in this protected " -"program" +"You must provide a reason to check for {{enrollmentsLabel}} in this " +"protected program." msgstr "" -"Describe the reason you are checking for enrollments in this protected " -"program" +"You must provide a reason to check for {{enrollmentsLabel}} in this " +"protected program." + +msgid "All activity will be logged." +msgstr "All activity will be logged." -msgid "Check for enrollments" -msgstr "Check for enrollments" +msgid "Reason to check for {{enrollmentsLabel}}" +msgstr "Reason to check for {{enrollmentsLabel}}" msgid "" -"You must provide a reason to check for enrollments in this protected " -"program." +"Describe the reason you are checking for {{enrollmentsLabel}} in this " +"protected program" msgstr "" -"You must provide a reason to check for enrollments in this protected " -"program." - -msgid "All activity will be logged." -msgstr "All activity will be logged." +"Describe the reason you are checking for {{enrollmentsLabel}} in this " +"protected program" msgid "Unsaved changes" msgstr "Unsaved changes" @@ -1338,14 +1325,14 @@ msgstr "Unsaved changes" msgid "Continue data entry" msgstr "Continue data entry" -msgid "Enrollment actions" -msgstr "Enrollment actions" +msgid "{{enrollmentLabel}} actions" +msgstr "{{enrollmentLabel}} actions" msgid "We are processing your request." msgstr "We are processing your request." -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 {{enrollmentLabel}} per {{tetName}} is allowed in this program" +msgstr "Only one {{enrollmentLabel}} per {{tetName}} is allowed in this program" msgid "Add new" msgstr "Add new" @@ -1359,26 +1346,26 @@ msgstr "Mark as cancelled" msgid "Mark incomplete" msgstr "Mark incomplete" -msgid "You do not have access to delete this enrollment" -msgstr "You do not have access to delete this enrollment" +msgid "You do not have access to delete this {{enrollmentLabel}}" +msgstr "You do not have access to delete this {{enrollmentLabel}}" -msgid "Delete enrollment" -msgstr "Delete enrollment" +msgid "Delete {{enrollmentLabel}}" +msgstr "Delete {{enrollmentLabel}}" -msgid "Are you sure you want to delete this enrollment?" -msgstr "Are you sure you want to delete this enrollment?" +msgid "Are you sure you want to delete this {{enrollmentLabel}}?" +msgstr "Are you sure you want to delete this {{enrollmentLabel}}?" -msgid "This will permanently remove the current enrollment." -msgstr "This will permanently remove the current enrollment." +msgid "This will permanently remove the current {{enrollmentLabel}}." +msgstr "This will permanently remove the current {{enrollmentLabel}}." -msgid "Yes, delete enrollment." -msgstr "Yes, delete enrollment." +msgid "Yes, delete {{enrollmentLabel}}." +msgstr "Yes, delete {{enrollmentLabel}}." -msgid "Remove mark for follow-up" -msgstr "Remove mark for follow-up" +msgid "Remove mark for {{followUpLabel}}" +msgstr "Remove mark for {{followUpLabel}}" -msgid "Mark for follow-up" -msgstr "Mark for follow-up" +msgid "Mark for {{followUpLabel}}" +msgstr "Mark for {{followUpLabel}}" msgid "Transfer" msgstr "Transfer" @@ -1386,8 +1373,8 @@ msgstr "Transfer" msgid "An error occurred while transferring ownership" msgstr "An error occurred while transferring ownership" -msgid "Existing dates for auto-generated events will not be updated." -msgstr "Existing dates for auto-generated events will not be updated." +msgid "Existing dates for auto-generated {{eventsLabel}} will not be updated." +msgstr "Existing dates for auto-generated {{eventsLabel}} will not be updated." msgid "Latitude" msgstr "Latitude" @@ -1417,40 +1404,36 @@ msgid "Set area" msgstr "Set area" msgid "" -"Transferring enrollment ownership from {{ownerOrgUnit}} to " +"Transferring {{enrollmentLabel}} ownership from {{ownerOrgUnit}} to " "{{newOrgUnit}}{{escape}}" msgstr "" -"Transferring enrollment ownership from {{ownerOrgUnit}} to " +"Transferring {{enrollmentLabel}} ownership from {{ownerOrgUnit}} to " "{{newOrgUnit}}{{escape}}" -msgid "" -"You will lose access to the enrollment when transferring ownership to " -"{{organisationUnit}}." -msgstr "" -"You will lose access to the enrollment when transferring ownership to " -"{{organisationUnit}}." - msgid "Transfer Ownership" msgstr "Transfer Ownership" msgid "" -"Choose the organisation unit to which enrollment ownership should be " -"transferred." +"Choose the {{orgUnitLabel}} to which {{enrollmentLabel}} ownership should " +"be transferred." msgstr "" -"Choose the organisation unit to which enrollment ownership should be " -"transferred." +"Choose the {{orgUnitLabel}} to which {{enrollmentLabel}} ownership should " +"be transferred." -msgid "Enrollment date" -msgstr "Enrollment date" +msgid "{{enrollmentLabel}} date" +msgstr "{{enrollmentLabel}} date" msgid "Incident date" msgstr "Incident date" -msgid "Enrollment widget could not be loaded. Please try again later" -msgstr "Enrollment widget could not be loaded. Please try again later" +msgid "{{enrollmentLabel}}" +msgstr "{{enrollmentLabel}}" + +msgid "{{enrollmentLabel}} widget could not be loaded. Please try again later" +msgstr "{{enrollmentLabel}} widget could not be loaded. Please try again later" -msgid "Follow-up" -msgstr "Follow-up" +msgid "{{followUpLabel}}" +msgstr "{{followUpLabel}}" msgid "Started at{{escape}}" msgstr "Started at{{escape}}" @@ -1467,11 +1450,8 @@ msgstr "Add coordinates" msgid "Add area" msgstr "Add area" -msgid "Program stage not found" -msgstr "Program stage not found" - -msgid "organisation unit could not be retrieved. Please try again later." -msgstr "organisation unit could not be retrieved. Please try again later." +msgid "{{orgUnitLabel}} could not be retrieved. Please try again later." +msgstr "{{orgUnitLabel}} could not be retrieved. Please try again later." msgid "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" @@ -1479,17 +1459,17 @@ msgstr "Saving to {{stageName}} for {{programName}} in {{orgUnitName}}" msgid "Saving to {{stageName}} for {{programName}}" msgstr "Saving to {{stageName}} for {{programName}}" -msgid "program or stage is invalid" -msgstr "program or stage is invalid" +msgid "Program or {{programStageLabel}} is invalid" +msgstr "Program or {{programStageLabel}} is invalid" -msgid "Notes about this enrollment" -msgstr "Notes about this enrollment" +msgid "{{notesLabel}} about this {{enrollmentLabel}}" +msgstr "{{notesLabel}} about this {{enrollmentLabel}}" -msgid "Write a note about this enrollment" -msgstr "Write a note about this enrollment" +msgid "Write a {{noteLabel}} about this {{enrollmentLabel}}" +msgstr "Write a {{noteLabel}} about this {{enrollmentLabel}}" -msgid "This enrollment doesn't have any notes" -msgstr "This enrollment doesn't have any notes" +msgid "This {{enrollmentLabel}} doesn't have any {{notesLabel}}" +msgstr "This {{enrollmentLabel}} doesn't have any {{notesLabel}}" msgid "Error" msgstr "Error" @@ -1497,29 +1477,29 @@ msgstr "Error" msgid "Warning" msgstr "Warning" -msgid "stage not found in rules execution" -msgstr "stage not found in rules execution" +msgid "{{programStageLabel}} not found in rules execution" +msgstr "{{programStageLabel}} not found in rules execution" -msgid "Delete event" -msgstr "Delete event" +msgid "Delete {{eventLabel}}" +msgstr "Delete {{eventLabel}}" -msgid "Deleting an event is permanent and cannot be undone." -msgstr "Deleting an event is permanent and cannot be undone." +msgid "Deleting an {{eventLabel}} is permanent and cannot be undone." +msgstr "Deleting an {{eventLabel}} is permanent and cannot be undone." -msgid "Are you sure you want to delete this event? " -msgstr "Are you sure you want to delete this event? " +msgid "Are you sure you want to delete this {{eventLabel}}? " +msgstr "Are you sure you want to delete this {{eventLabel}}? " -msgid "Yes, delete event" -msgstr "Yes, delete event" +msgid "Yes, delete {{eventLabel}}" +msgstr "Yes, delete {{eventLabel}}" -msgid "Go to “Schedule” tab to reschedule this event" -msgstr "Go to “Schedule” tab to reschedule this event" +msgid "Go to “Schedule” tab to reschedule this {{eventLabel}}" +msgstr "Go to “Schedule” tab to reschedule this {{eventLabel}}" -msgid "Scheduled date cannot be changed for {{ eventStatus }} events" -msgstr "Scheduled date cannot be changed for {{ eventStatus }} events" +msgid "Scheduled date cannot be changed for {{ eventStatus }} {{eventsLabel}}" +msgstr "Scheduled date cannot be changed for {{ eventStatus }} {{eventsLabel}}" -msgid "You do not have access to uncomplete this event" -msgstr "You do not have access to uncomplete this event" +msgid "You do not have access to uncomplete this {{eventLabel}}" +msgstr "You do not have access to uncomplete this {{eventLabel}}" msgid "Geometry (Area)" msgstr "Geometry (Area)" @@ -1533,14 +1513,14 @@ msgstr "Polygon captured" msgid "No polygon captured" msgstr "No polygon captured" -msgid "Event completed" -msgstr "Event completed" +msgid "{{eventLabel}} completed" +msgstr "{{eventLabel}} completed" -msgid "Notes about this event" -msgstr "Notes about this event" +msgid "{{notesLabel}} about this {{eventLabel}}" +msgstr "{{notesLabel}} about this {{eventLabel}}" -msgid "Write a note about this event" -msgstr "Write a note about this event" +msgid "Write a {{noteLabel}} about this {{eventLabel}}" +msgstr "Write a {{noteLabel}} about this {{eventLabel}}" msgid "after" msgstr "after" @@ -1559,42 +1539,27 @@ 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 an {{eventLabel}} in {{stageName}} for {{programName}} in " +"{{orgUnitName}}" +msgstr "" +"Scheduling an {{eventLabel}} in {{stageName}} for {{programName}} in " +"{{orgUnitName}}" -msgid "Scheduling an event in {{stageName}} for {{programName}}" -msgstr "Scheduling an event in {{stageName}} for {{programName}}" +msgid "Scheduling an {{eventLabel}} in {{stageName}} for {{programName}}" +msgstr "Scheduling an {{eventLabel}} in {{stageName}} for {{programName}}" msgid "Schedule info" msgstr "Schedule info" -msgid "Event notes" -msgstr "Event notes" +msgid "{{eventLabel}} {{notesLabel}}" +msgstr "{{eventLabel}} {{notesLabel}}" -msgid "Write a note about this scheduled event" -msgstr "Write a note about this scheduled event" - -msgid "Program or stage is invalid" -msgstr "Program or stage is invalid" +msgid "Write a {{noteLabel}} about this scheduled {{eventLabel}}" +msgstr "Write a {{noteLabel}} about this scheduled {{eventLabel}}" msgid "Feedback" msgstr "Feedback" @@ -1602,8 +1567,8 @@ msgstr "Feedback" msgid "Indicators" msgstr "Indicators" -msgid "Save note" -msgstr "Save note" +msgid "Save {{noteLabel}}" +msgstr "Save {{noteLabel}}" msgid "Edit {{trackedEntityName}}" msgstr "Edit {{trackedEntityName}}" @@ -1614,8 +1579,12 @@ msgstr "Save changes" msgid "Change information about this {{trackedEntityName}} here." msgstr "Change information about this {{trackedEntityName}} here." -msgid "Information about this enrollment can be edited in the Enrollment widget." -msgstr "Information about this enrollment can be edited in the Enrollment widget." +msgid "" +"Information about this {{enrollmentLabel}} can be edited in the " +"{{enrollmentLabel}} widget." +msgstr "" +"Information about this {{enrollmentLabel}} can be edited in the " +"{{enrollmentLabel}} widget." msgid "{{trackedEntityName}} profile" msgstr "{{trackedEntityName}} profile" @@ -1679,11 +1648,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." +"{{enrollmentsLabel}} and {{eventsLabel}} in all programs." msgstr "" "Are you sure you want to delete this {{trackedEntityTypeName}}? This will " "permanently remove the {{trackedEntityTypeName}} and all its associated " -"enrollments and events in all programs." +"{{enrollmentsLabel}} and {{eventsLabel}} in all programs." msgid "There was a problem deleting the {{trackedEntityTypeName}}" msgstr "There was a problem deleting the {{trackedEntityTypeName}}" @@ -1697,50 +1666,50 @@ 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 {{attributeLabel}} configured for {{trackedEntityTypeName}}" +msgstr "No {{attributeLabel}} configured for {{trackedEntityTypeName}}" -msgid "No attributes configured" -msgstr "No attributes configured" +msgid "No {{attributeLabel}} configured" +msgstr "No {{attributeLabel}} configured" msgid "{{trackedEntityTypeName}} profile" msgstr "{{trackedEntityTypeName}} profile" -msgid "Choose a {{linkableStageLabel}} event" -msgstr "Choose a {{linkableStageLabel}} event" +msgid "Choose a {{linkableStageLabel}} {{eventLabel}}" +msgstr "Choose a {{linkableStageLabel}} {{eventLabel}}" + +msgid "Select an {{eventLabel}}" +msgstr "Select an {{eventLabel}}" -msgid "Select an event" -msgstr "Select an event" +msgid "{{ linkableStageLabel }} can only have one {{eventLabel}}" +msgstr "{{ linkableStageLabel }} can only have one {{eventLabel}}" -msgid "{{ linkableStageLabel }} can only have one event" -msgstr "{{ linkableStageLabel }} can only have one event" +msgid "{{ linkableStageLabel }} has no linkable {{eventsLabel}}" +msgstr "{{ linkableStageLabel }} has no linkable {{eventsLabel}}" -msgid "{{ linkableStageLabel }} has no linkable events" -msgstr "{{ linkableStageLabel }} has no linkable events" +msgid "Link to an existing {{eventLabel}}" +msgstr "Link to an existing {{eventLabel}}" msgid "Actions - {{relationshipName}}" msgstr "Actions - {{relationshipName}}" -msgid "Ambiguous relationships, contact system administrator" -msgstr "Ambiguous relationships, contact system administrator" +msgid "Ambiguous {{relationshipsLabel}}, contact system administrator" +msgstr "Ambiguous {{relationshipsLabel}}, contact system administrator" msgid "Enter details" msgstr "Enter details" -msgid "Linked event" -msgstr "Linked event" +msgid "Linked {{eventLabel}}" +msgstr "Linked {{eventLabel}}" msgid "Enter details now" msgstr "Enter details now" -msgid "Link to an existing event" -msgstr "Link to an existing event" +msgid "The {{eventLabel}} was successfully linked" +msgstr "The {{eventLabel}} 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 {{eventLabel}}" +msgstr "An error occurred while linking the {{eventLabel}}" msgid "Scheduled date" msgstr "Scheduled date" @@ -1751,29 +1720,33 @@ msgstr "Report date" msgid "Please enter a date" msgstr "Please enter a date" -msgid "Please select a valid event" -msgstr "Please select a valid event" +msgid "Please select a valid {{eventLabel}}" +msgstr "Please select a valid {{eventLabel}}" -msgid "This stage can only have one event" -msgstr "This stage can only have one event" +msgid "This {{programStageLabel}} can only have one {{eventLabel}}" +msgstr "This {{programStageLabel}} can only have one {{eventLabel}}" -msgid "New {{ eventName }} event" -msgstr "New {{ eventName }} event" +msgid "New {{ eventName }} {{eventLabel}}" +msgstr "New {{ eventName }} {{eventLabel}}" -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. {{eventLabel}} cannot be " +"deleted" +msgstr "" +"{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be " +"deleted" -msgid "This event is outside the edit period" -msgstr "This event is outside the edit period" +msgid "This {{eventLabel}} is outside the edit period" +msgstr "This {{eventLabel}} is outside the edit period" -msgid "An error occurred while deleting the event" -msgstr "An error occurred while deleting the event" +msgid "An error occurred while deleting the {{eventLabel}}" +msgstr "An error occurred while deleting the {{eventLabel}}" -msgid "Are you sure you want to delete this event?" -msgstr "Are you sure you want to delete this event?" +msgid "Are you sure you want to delete this {{eventLabel}}?" +msgstr "Are you sure you want to delete this {{eventLabel}}?" -msgid "An error occurred when updating event status" -msgstr "An error occurred when updating event status" +msgid "An error occurred when updating {{eventLabel}} status" +msgstr "An error occurred when updating {{eventLabel}} status" msgid "Unskip" msgstr "Unskip" @@ -1781,8 +1754,8 @@ 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 "To open this {{eventLabel}}, please wait until saving is complete" +msgstr "To open this {{eventLabel}}, please wait until saving is complete" msgid "Show {{ rest }} more" msgstr "Show {{ rest }} more" @@ -1790,13 +1763,8 @@ msgstr "Show {{ rest }} more" msgid "Go to full {{ eventName }}" msgstr "Go to full {{ eventName }}" -msgid "Events could not be retrieved. Please try again later." -msgstr "Events could not be retrieved. Please try again later." - -msgid "{{ count }} event" -msgid_plural "{{ count }} event" -msgstr[0] "{{ count }} event" -msgstr[1] "{{count}} events" +msgid "{{eventsLabel}} could not be retrieved. Please try again later." +msgstr "{{eventsLabel}} could not be retrieved. Please try again later." msgid "{{ overdueEvents }} overdue" msgstr "{{ overdueEvents }} overdue" @@ -1804,62 +1772,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 "No {{programStagesLabel}} found in this program" +msgstr "No {{programStagesLabel}} found in this program" -msgid "Stages and Events" -msgstr "Stages and Events" +msgid "{{programStagesLabel}} and {{eventsLabel}}" +msgstr "{{programStagesLabel}} and {{eventsLabel}}" -msgid "An error occurred while unlinking and deleting the event." -msgstr "An error occurred while unlinking and deleting the event." +msgid "An error occurred while unlinking and deleting the {{eventLabel}}." +msgstr "An error occurred while unlinking and deleting the {{eventLabel}}." -msgid "Unlink and delete linked event" -msgstr "Unlink and delete linked event" +msgid "Unlink and delete linked {{eventLabel}}" +msgstr "Unlink and delete linked {{eventLabel}}" -msgid "Are you sure you want to remove the link and delete the linked 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 " +"{{eventLabel}}?" +msgstr "" +"Are you sure you want to remove the link and delete the linked " +"{{eventLabel}}?" msgid "" -"This action permanently removes the link, linked event, and all related " -"data." +"This action permanently removes the link, linked {{eventLabel}}, and all " +"related data." msgstr "" -"This action permanently removes the link, linked event, and all related " -"data." +"This action permanently removes the link, linked {{eventLabel}}, and all " +"related data." -msgid "Yes, unlink and delete linked event" -msgstr "Yes, unlink and delete linked event" +msgid "Yes, unlink and delete linked {{eventLabel}}" +msgstr "Yes, unlink and delete linked {{eventLabel}}" -msgid "Unlink event" -msgstr "Unlink event" +msgid "Unlink {{eventLabel}}" +msgstr "Unlink {{eventLabel}}" -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 {{eventsLabel}}?" +msgstr "Are you sure you want to remove the link between these {{eventsLabel}}?" -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 {{eventLabel}} will " +"remain." +msgstr "" +"This action removes the link itself, but the linked {{eventLabel}} will " +"remain." -msgid "Yes, unlink event" -msgstr "Yes, unlink event" +msgid "Yes, unlink {{eventLabel}}" +msgstr "Yes, unlink {{eventLabel}}" -msgid "View linked event" -msgstr "View linked event" +msgid "View linked {{eventLabel}}" +msgstr "View linked {{eventLabel}}" -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 {{eventsLabel}}" +msgstr "You do not have access to remove the link between these {{eventsLabel}}" -msgid "You do not have access to remove the link and delete the linked 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 " +"{{eventLabel}}" +msgstr "" +"You do not have access to remove the link and delete the linked " +"{{eventLabel}}" msgid "An error occurred while loading the widget." msgstr "An error occurred while loading the widget." -msgid "" -"This {{stageName}} event is linked to a {{linkedStageName}} event. Review " -"the linked event details before entering data below" -msgstr "" -"This {{stageName}} event is linked to a {{linkedStageName}} event. Review " -"the linked event details before entering data below" - msgid "Scheduled" msgstr "Scheduled" @@ -1905,50 +1878,51 @@ msgstr "File" msgid "Image" msgstr "Image" -msgid "New {{trackedEntityTypeName}} relationship" -msgstr "New {{trackedEntityTypeName}} relationship" +msgid "New {{trackedEntityTypeName}} {{relationshipLabel}}" +msgstr "New {{trackedEntityTypeName}} {{relationshipLabel}}" 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 {{relationshipLabel}}" +msgstr "Go back without saving {{relationshipLabel}}" -msgid "New Relationship" -msgstr "New Relationship" +msgid "New {{relationshipLabel}}" +msgstr "New {{relationshipLabel}}" msgid "Link to an existing {{tetName}}" msgstr "Link to an existing {{tetName}}" -msgid "An error occurred while adding the 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 "An error occurred while adding the {{relationshipLabel}}" +msgstr "An error occurred while adding the {{relationshipLabel}}" -msgid "{{trackedEntityTypeName}} relationships" -msgstr "{{trackedEntityTypeName}} relationships" +msgid "" +"Something went wrong while loading {{relationshipLabel}}. Please try again " +"later." +msgstr "" +"Something went wrong while loading {{relationshipLabel}}. Please try again " +"later." -msgid "Delete relationship" -msgstr "Delete relationship" +msgid "{{trackedEntityTypeName}} {{relationshipLabel}}" +msgstr "{{trackedEntityTypeName}} {{relationshipLabel}}" -msgid "Deleting the relationship is permanent and cannot be undone." -msgstr "Deleting the relationship is permanent and cannot be undone." +msgid "Delete {{relationshipLabel}}" +msgstr "Delete {{relationshipLabel}}" -msgid "Are you sure you want to delete this relationship?" -msgstr "Are you sure you want to delete this relationship?" +msgid "Deleting the {{relationshipLabel}} is permanent and cannot be undone." +msgstr "Deleting the {{relationshipLabel}} is permanent and cannot be undone." -msgid "Yes, delete relationship" -msgstr "Yes, delete relationship" +msgid "Are you sure you want to delete this {{relationshipLabel}}?" +msgstr "Are you sure you want to delete this {{relationshipLabel}}?" -msgid "An error occurred while deleting the relationship." -msgstr "An error occurred while deleting the relationship." +msgid "Yes, delete {{relationshipLabel}}" +msgstr "Yes, delete {{relationshipLabel}}" -msgid "To open this relationship, please wait until saving is complete" -msgstr "To open this relationship, please wait until saving is complete" +msgid "An error occurred while deleting the {{relationshipLabel}}." +msgstr "An error occurred while deleting the {{relationshipLabel}}." -msgid "This enrollment doesn't have any relationships" -msgstr "This enrollment doesn't have any relationships" +msgid "This {{enrollmentLabel}} doesn't have any {{relationshipsLabel}}" +msgstr "This {{enrollmentLabel}} doesn't have any {{relationshipsLabel}}" msgid "Type" msgstr "Type" @@ -1956,8 +1930,8 @@ msgstr "Type" msgid "Created date" msgstr "Created date" -msgid "Program stage name" -msgstr "Program stage name" +msgid "{{programStageLabel}} name" +msgstr "{{programStageLabel}} name" msgid "Working list could not be loaded" msgstr "Working list could not be loaded" @@ -1968,26 +1942,26 @@ 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 {{eventsLabel}}" +msgstr "You do not have access to complete {{eventsLabel}}" msgid "There is a bulk data entry with unsaved changes" msgstr "There is a bulk data entry with unsaved changes" -msgid "Complete events" -msgstr "Complete events" +msgid "Complete {{eventsLabel}}" +msgstr "Complete {{eventsLabel}}" -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 {{eventsLabel}} in selection?" +msgstr "Are you sure you want to complete all active {{eventsLabel}} 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 {{eventsLabel}} to complete in the current selection." +msgstr "There are no active {{eventsLabel}} to complete in the current selection." -msgid "Error completing events" -msgstr "Error completing events" +msgid "Error completing {{eventsLabel}}" +msgstr "Error completing {{eventsLabel}}" -msgid "There was an error completing the events." -msgstr "There was an error completing the events." +msgid "There was an error completing the {{eventsLabel}}." +msgstr "There was an error completing the {{eventsLabel}}." msgid "Details (Advanced)" msgstr "Details (Advanced)" @@ -1995,154 +1969,171 @@ 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 {{eventsLabel}}" +msgstr "An error occurred while completing {{eventsLabel}}" -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 {{eventsLabel}}" +msgstr "You do not have access to delete {{eventsLabel}}" -msgid "An error occurred while deleting the events" -msgstr "An error occurred while deleting the events" +msgid "An error occurred while deleting the {{eventsLabel}}" +msgstr "An error occurred while deleting the {{eventsLabel}}" -msgid "Delete events" -msgstr "Delete events" +msgid "Delete {{eventsLabel}}" +msgstr "Delete {{eventsLabel}}" msgid "This cannot be undone." msgstr "This cannot be undone." -msgid "Are you sure you want to delete the selected events?" -msgstr "Are you sure you want to delete the selected events?" +msgid "Are you sure you want to delete the selected {{eventsLabel}}?" +msgstr "Are you sure you want to delete the selected {{eventsLabel}}?" -msgid "Owner organisation unit" -msgstr "Owner organisation unit" +msgid "Owner {{orgUnitLabel}}" +msgstr "Owner {{orgUnitLabel}}" msgid "Registration Date" msgstr "Registration Date" -msgid "Enrollment status" -msgstr "Enrollment status" +msgid "{{eventLabel}} {{orgUnitLabel}}" +msgstr "{{eventLabel}} {{orgUnitLabel}}" -msgid "Follow up" -msgstr "Follow up" +msgid "{{enrollmentLabel}} status" +msgstr "{{enrollmentLabel}} status" -msgid "Choose a program stage to filter by {{label}}" -msgstr "Choose a program stage to filter by {{label}}" +msgid "Choose a {{programStageLabel}} to filter by {{label}}" +msgstr "Choose a {{programStageLabel}} to filter by {{label}}" -msgid "You do not have access to bulk complete 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." -msgstr "" -"Some enrollments were completed successfully, but there was an error while " -"completing the rest. Please see the details below." +msgid "You do not have access to bulk complete {{enrollmentsLabel}}" +msgstr "You do not have access to bulk complete {{enrollmentsLabel}}" msgid "" -"There was an error while completing the enrollments. Please see the details " -"below." +"Some {{enrollmentsLabel}} were completed successfully, but there was an " +"error while completing the rest. Please see the details below." msgstr "" -"There was an error while completing the enrollments. Please see the details " -"below." +"Some {{enrollmentsLabel}} were completed successfully, but there was an " +"error while completing the rest. Please see the details below." msgid "" -"An unexpected error occurred while fetching the enrollments. Please try " -"again." +"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " +"Please try again." msgstr "" -"An unexpected error occurred while fetching the enrollments. Please try " -"again." +"An unexpected error occurred while fetching the {{enrollmentsLabel}}. " +"Please try again." -msgid "There are currently no active enrollments in the selection." -msgstr "There are currently no active enrollments in the selection." +msgid "There are currently no active {{enrollmentsLabel}} in the selection." +msgstr "There are currently no active {{enrollmentsLabel}} in the selection." -msgid "All enrollments are already completed or cancelled." -msgstr "All enrollments are already completed or cancelled." +msgid "All {{enrollmentsLabel}} are already completed or cancelled." +msgstr "All {{enrollmentsLabel}} are already completed or cancelled." -msgid "This action will complete {{count}} active enrollment in your selection." -msgid_plural "This action will complete {{count}} active enrollment in your selection." -msgstr[0] "This action will complete {{count}} active enrollment in your selection." -msgstr[1] "This action will complete {{count}} active enrollments in your selection." +msgid "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgid_plural "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgstr[0] "" +"This action will complete {{count}} active {{enrollmentLabel}} in your " +"selection." +msgstr[1] "" +"This action will complete {{count}} active {{enrollmentsLabel}} in your " +"selection." -msgid "{{count}} enrollment already marked as completed will not be changed." -msgid_plural "{{count}} enrollment already marked as completed will not be changed." -msgstr[0] "{{count}} enrollment already marked as completed will not be changed." -msgstr[1] "{{count}} enrollments already marked as completed will not be changed." +msgid "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgid_plural "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgstr[0] "" +"{{count}} {{enrollmentLabel}} already marked as completed will not be " +"changed." +msgstr[1] "" +"{{count}} {{enrollmentsLabel}} already marked as completed will not be " +"changed." -msgid "Mark all events within enrollments as complete" -msgstr "Mark all events within enrollments as complete" +msgid "Mark all {{eventsLabel}} within {{enrollmentsLabel}} as complete" +msgstr "Mark all {{eventsLabel}} within {{enrollmentsLabel}} as complete" -msgid "Complete enrollments" -msgstr "Complete enrollments" +msgid "Complete {{enrollmentsLabel}}" +msgstr "Complete {{enrollmentsLabel}}" -msgid "Error completing enrollments" -msgstr "Error completing enrollments" +msgid "Error completing {{enrollmentsLabel}}" +msgstr "Error completing {{enrollmentsLabel}}" -msgid "No active enrollments to complete" -msgstr "No active enrollments to complete" +msgid "No active {{enrollmentsLabel}} to complete" +msgstr "No active {{enrollmentsLabel}} to complete" -msgid "Complete {{count}} enrollment" -msgid_plural "Complete {{count}} enrollment" -msgstr[0] "Complete {{count}} enrollment" -msgstr[1] "Complete {{count}} enrollments" +msgid "Complete {{count}} {{enrollmentLabel}}" +msgid_plural "Complete {{count}} {{enrollmentLabel}}" +msgstr[0] "Complete {{count}} {{enrollmentLabel}}" +msgstr[1] "Complete {{count}} {{enrollmentsLabel}}" -msgid "An error occurred when completing the enrollments" -msgstr "An error occurred when completing the enrollments" +msgid "An error occurred when completing the {{enrollmentsLabel}}" +msgstr "An error occurred when completing the {{enrollmentsLabel}}" -msgid "An unknown error occurred when completing enrollments" -msgstr "An unknown error occurred when completing enrollments" +msgid "An unknown error occurred when completing {{enrollmentsLabel}}" +msgstr "An unknown error occurred when completing {{enrollmentsLabel}}" -msgid "You do not have access to delete enrollments" -msgstr "You do not have access to delete enrollments" +msgid "You do not have access to delete {{enrollmentsLabel}}" +msgstr "You do not have access to delete {{enrollmentsLabel}}" -msgid "Delete enrollments" -msgstr "Delete enrollments" +msgid "Delete {{enrollmentsLabel}}" +msgstr "Delete {{enrollmentsLabel}}" -msgid "Delete selected enrollments" -msgstr "Delete selected enrollments" +msgid "Delete selected {{enrollmentsLabel}}" +msgstr "Delete selected {{enrollmentsLabel}}" -msgid "An error occurred while loading the selected enrollments. Please try again." -msgstr "An error occurred while loading the selected enrollments. Please try again." +msgid "" +"An error occurred while loading the selected {{enrollmentsLabel}}. Please " +"try again." +msgstr "" +"An error occurred while loading the selected {{enrollmentsLabel}}. Please " +"try again." msgid "" -"This action will permanently delete the selected enrollments, including all " -"associated data and events." +"This action will permanently delete the selected {{enrollmentsLabel}}, " +"including all associated data and {{eventsLabel}}." msgstr "" -"This action will permanently delete the selected enrollments, including all " -"associated data and events." +"This action will permanently delete the selected {{enrollmentsLabel}}, " +"including all associated data and {{eventsLabel}}." -msgid "Active enrollments ({{count}})" -msgid_plural "Active enrollments ({{count}})" -msgstr[0] "Active enrollments ({{count}})" -msgstr[1] "Active enrollments ({{count}})" +msgid "Active {{enrollmentsLabel}} ({{count}})" +msgid_plural "Active {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Active {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Active {{enrollmentsLabel}} ({{count}})" -msgid "Completed enrollments ({{count}})" -msgid_plural "Completed enrollments ({{count}})" -msgstr[0] "Completed enrollments ({{count}})" -msgstr[1] "Completed enrollments ({{count}})" +msgid "Completed {{enrollmentsLabel}} ({{count}})" +msgid_plural "Completed {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Completed {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Completed {{enrollmentsLabel}} ({{count}})" -msgid "Cancelled enrollments ({{count}})" -msgid_plural "Cancelled enrollments ({{count}})" -msgstr[0] "Cancelled enrollments ({{count}})" -msgstr[1] "Cancelled enrollments ({{count}})" +msgid "Cancelled {{enrollmentsLabel}} ({{count}})" +msgid_plural "Cancelled {{enrollmentsLabel}} ({{count}})" +msgstr[0] "Cancelled {{enrollmentsLabel}} ({{count}})" +msgstr[1] "Cancelled {{enrollmentsLabel}} ({{count}})" -msgid "Delete {{count}} enrollment" -msgid_plural "Delete {{count}} enrollment" -msgstr[0] "Delete {{count}} enrollment" -msgstr[1] "Delete {{count}} enrollments" +msgid "Delete {{count}} {{enrollmentLabel}}" +msgid_plural "Delete {{count}} {{enrollmentLabel}}" +msgstr[0] "Delete {{count}} {{enrollmentLabel}}" +msgstr[1] "Delete {{count}} {{enrollmentsLabel}}" -msgid "An error occurred when deleting enrollments" -msgstr "An error occurred when deleting enrollments" +msgid "An error occurred when deleting {{enrollmentsLabel}}" +msgstr "An error occurred when deleting {{enrollmentsLabel}}" -msgid "Delete {{ trackedEntityName }} with all enrollments" -msgstr "Delete {{ trackedEntityName }} with all enrollments" +msgid "Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}" +msgstr "Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}" msgid "Delete {{count}} {{ trackedEntityName }}" msgid_plural "Delete {{count}} {{ trackedEntityName }}" msgstr[0] "Delete {{count}} {{ trackedEntityName }}" msgstr[1] "Delete {{count}} {{ trackedEntityName }}" -msgid "Deleting records will also delete any associated enrollments and events." -msgstr "Deleting records will also delete any associated enrollments and events." +msgid "" +"Deleting records will also delete any associated {{enrollmentsLabel}} and " +"{{eventsLabel}}." +msgstr "" +"Deleting records will also delete any associated {{enrollmentsLabel}} and " +"{{eventsLabel}}." msgid "Are you sure you want to delete?" msgstr "Are you sure you want to delete?" @@ -2248,17 +2239,59 @@ msgstr "Visited" msgid "{{trackedEntityName}} in program{{escape}} {{programName}}" msgstr "{{trackedEntityName}} in program{{escape}} {{programName}}" +msgid "enrollment" +msgstr "enrollment" + +msgid "enrollments" +msgstr "enrollments" + +msgid "event" +msgstr "event" + +msgid "events" +msgstr "events" + +msgid "program stage" +msgstr "program stage" + +msgid "program stages" +msgstr "program stages" + +msgid "note" +msgstr "note" + +msgid "notes" +msgstr "notes" + +msgid "relationship" +msgstr "relationship" + +msgid "relationships" +msgstr "relationships" + +msgid "attribute" +msgstr "attribute" + +msgid "attributes" +msgstr "attributes" + +msgid "organisation unit" +msgstr "organisation unit" + +msgid "follow-up" +msgstr "follow-up" + msgid "Program not found" msgstr "Program not found" msgid "Program is not a tracker program" msgstr "Program is not a tracker program" -msgid "Could not save event" -msgstr "Could not save event" +msgid "Could not save {{eventLabel}}" +msgstr "Could not save {{eventLabel}}" -msgid "Could not delete event" -msgstr "Could not delete event" +msgid "Could not delete {{eventLabel}}" +msgstr "Could not delete {{eventLabel}}" msgid "Could not save working list" msgstr "Could not save working list" @@ -2269,32 +2302,32 @@ msgstr "Could not add working list" msgid "Could not delete working list" msgstr "Could not delete working list" -msgid "Organisation unit search failed." -msgstr "Organisation unit search failed." +msgid "Search failed. Please try again." +msgstr "Search failed. Please try again." msgid "Error saving tracked entity instance" msgstr "Error saving tracked entity instance" -msgid "Error saving enrollment" -msgstr "Error saving enrollment" +msgid "Error saving {{enrollmentLabel}}" +msgstr "Error saving {{enrollmentLabel}}" -msgid "Error saving the enrollment event" -msgstr "Error saving the enrollment event" +msgid "Error saving the {{enrollmentLabel}} {{eventLabel}}" +msgstr "Error saving the {{enrollmentLabel}} {{eventLabel}}" -msgid "Error deleting the enrollment event" -msgstr "Error deleting the enrollment event" +msgid "Error deleting the {{enrollmentLabel}} {{eventLabel}}" +msgstr "Error deleting the {{enrollmentLabel}} {{eventLabel}}" -msgid "Error editing the event, the changes made were not saved" -msgstr "Error editing the event, the changes made were not saved" +msgid "Error editing the {{eventLabel}}, the changes made were not saved" +msgstr "Error editing the {{eventLabel}}, the changes made were not saved" msgid "Error updating the Assignee" msgstr "Error updating the Assignee" -msgid "Could not save enrollment note" -msgstr "Could not save enrollment note" +msgid "Could not save {{enrollmentLabel}} {{noteLabel}}" +msgstr "Could not save {{enrollmentLabel}} {{noteLabel}}" -msgid "Could not save event note" -msgstr "Could not save event note" +msgid "Could not save {{eventLabel}} {{noteLabel}}" +msgstr "Could not save {{eventLabel}} {{noteLabel}}" msgid "There was an error fetching metadata" msgstr "There was an error fetching metadata" @@ -2317,6 +2350,9 @@ msgstr "Please provide a valid age" msgid "Please provide a valid phone number" msgstr "Please provide a valid phone number" +msgid "Please provide a valid selection." +msgstr "Please provide a valid selection." + msgid "This value already exists" msgstr "This value already exists" @@ -2343,20 +2379,20 @@ msgid "Please enter a time" msgstr "Please enter a time" msgid "" -"The following attribute type is not supported for searching and has been " -"hidden" +"The following {{attributeLabel}} type is not supported for searching and " +"has been hidden" msgid_plural "" -"The following attribute type is not supported for searching and has been " -"hidden" +"The following {{attributeLabel}} type is not supported for searching and " +"has been hidden" msgstr[0] "" -"The following attribute type is not supported for searching and has been " -"hidden" +"The following {{attributeLabel}} type is not supported for searching and " +"has been hidden" msgstr[1] "" -"The following attribute types are not supported for searching and have been " -"hidden" +"The following {{attributeLabel}} types are not supported for searching and " +"have been hidden" -msgid "Some attributes are hidden" -msgstr "Some attributes are hidden" +msgid "Some {{attributeLabel}} are hidden" +msgstr "Some {{attributeLabel}} are hidden" msgid "Set coordinate" msgstr "Set coordinate" diff --git a/src/core_modules/capture-core-utils/featuresSupport/support.ts b/src/core_modules/capture-core-utils/featuresSupport/support.ts index baeb663dae..90274517ae 100644 --- a/src/core_modules/capture-core-utils/featuresSupport/support.ts +++ b/src/core_modules/capture-core-utils/featuresSupport/support.ts @@ -6,6 +6,7 @@ export const FEATURES = Object.freeze({ orgUnitReplaceOuQueryParam: 'orgUnitReplaceOuQueryParam', enrollmentStatusReplaceProgramStatusQueryParam: 'enrollmentStatusReplaceProgramStatusQueryParam', emptyValueFilter: 'emptyValueFilter', + customTerminologyPlurals: 'customTerminologyPlurals', }); const MINOR_VERSION_SUPPORT = Object.freeze({ @@ -16,6 +17,7 @@ const MINOR_VERSION_SUPPORT = Object.freeze({ [FEATURES.orgUnitReplaceOuQueryParam]: 42, [FEATURES.enrollmentStatusReplaceProgramStatusQueryParam]: 42, [FEATURES.emptyValueFilter]: 42, + [FEATURES.customTerminologyPlurals]: 43, }); export const hasAPISupportForFeature = (minorVersion: string | number, featureName: string) => diff --git a/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts b/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts index 0184db6f88..fdbe04456c 100644 --- a/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts +++ b/src/core_modules/capture-core-utils/string/capitalizeFirstLetter.ts @@ -1,5 +1,11 @@ +import i18n from '@dhis2/d2-i18n'; + export function capitalizeFirstLetter(text: string) { - const first = text.charAt(0).toLocaleUpperCase(); - const rest = text.slice(1); - return first + rest; + if (!text) return text; + const locale = (i18n as any).language ?? 'en'; + try { + return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1); + } catch { + return text.charAt(0).toUpperCase() + text.slice(1); + } } diff --git a/src/core_modules/capture-core/HOC/withCustomLabels.tsx b/src/core_modules/capture-core/HOC/withCustomLabels.tsx new file mode 100644 index 0000000000..912cffde7c --- /dev/null +++ b/src/core_modules/capture-core/HOC/withCustomLabels.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; +import { useTermLabel, type TermRequest } from '../metaData'; + +export const withCustomLabels = + (requests: ReadonlyArray) => + (InnerComponent: React.ComponentType) => + (props: any) => { + const { programId, stageId } = props; + const labels = useTermLabel(requests, { programId, stageId }); + const capitalized = Object.fromEntries( + Object.entries(labels).map(([key, value]) => [key, capitalizeFirstLetter(value)]), + ); + return ; + }; 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..86c4ec969c 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 { LabelKeys, useTermLabel } from '../../../../metaData'; type Props = { programId: string; @@ -10,30 +11,35 @@ type Props = { page: string; }; -const DefaultFilterLabels = { - default: i18n.t('Program overview'), - active: i18n.t('Active enrollments'), - complete: i18n.t('Completed enrollments'), - cancelled: i18n.t('Cancelled enrollments'), -}; - -const getWorkingListLabel = (selectedTemplate: any, selectedTemplateId: string) => { +const getWorkingListLabel = ( + selectedTemplate: any, + selectedTemplateId: string, + defaultFilterLabels: Record, +) => { if (selectedTemplate && !selectedTemplate.isDefault) { return selectedTemplate.name; } if (selectedTemplateId && !selectedTemplate) { - return DefaultFilterLabels[selectedTemplateId as keyof typeof DefaultFilterLabels]; + return defaultFilterLabels[selectedTemplateId as keyof typeof defaultFilterLabels]; } return i18n.t('Program overview'); }; export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) => { + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural]); const workingListTemplates = useSelector(({ workingListsTemplates }: any) => workingListsTemplates?.teiList); const workingListProgramId = useSelector(({ workingListsContext }: any) => workingListsContext?.teiList?.programIdView); const { selectedTemplateId, loading: isLoadingTemplates, templates } = workingListTemplates ?? {}; const selectedTemplate = templates?.find(({ id }: any) => id === selectedTemplateId); const isSameProgram = workingListProgramId === programId; + const defaultFilterLabels = useMemo(() => ({ + default: i18n.t('Program overview'), + active: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + complete: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + cancelled: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + }), [enrollmentsLabel]); + const label = useMemo(() => { if (page === breadcrumbsKeys.SEARCH_PAGE) { return i18n.t('Search'); @@ -44,7 +50,7 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) } if (isSameProgram) { - return getWorkingListLabel(selectedTemplate, selectedTemplateId); + return getWorkingListLabel(selectedTemplate, selectedTemplateId, defaultFilterLabels); } if (!displayFrontPageList) { @@ -58,6 +64,7 @@ export const useOriginLabel = ({ programId, displayFrontPageList, page }: Props) selectedTemplate, selectedTemplateId, page, + defaultFilterLabels, ]); return { diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx index 2fc98e432e..3ef28fa872 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EnrollmentBreadcrumb/EnrollmentBreadcrumb.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useMemo, useState, ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { useCallback, useMemo, useState, ComponentType } from 'react'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { colors } from '@dhis2/ui'; +import { LabelKeys, useTermLabel } from '../../../metaData'; import { DirectionalChevron } from '../../../utils/rtl'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; import { BreadcrumbItem } from '../common/BreadcrumbItem'; @@ -68,6 +69,10 @@ const BreadcrumbsPlain = ({ classes, }: Props) => { const [openWarning, setOpenWarning] = useState(null); + const { enrollmentLabel, eventLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.eventSingular], + { programId }, + ); const { label } = useWorkingListLabel({ programId, @@ -101,14 +106,14 @@ const BreadcrumbsPlain = ({ { key: pageKeys.OVERVIEW, onClick: () => handleNavigation(onBackToDashboard, pageKeys.OVERVIEW), - label: i18n.t('Enrollment dashboard'), + label: i18n.t('{{enrollmentLabel}} dashboard', { enrollmentLabel }), selected: page === pageKeys.OVERVIEW, condition: true, }, { key: pageKeys.VIEW_EVENT, onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT), - label: i18n.t('View event'), + label: i18n.t('View {{eventLabel}}', { eventLabel }), selected: page === pageKeys.VIEW_EVENT, condition: page === pageKeys.VIEW_EVENT || (page === pageKeys.EDIT_EVENT && !eventIsScheduled(eventStatus)), @@ -116,14 +121,14 @@ const BreadcrumbsPlain = ({ { key: pageKeys.EDIT_EVENT, onClick: () => undefined, - label: i18n.t('Edit event'), + label: i18n.t('Edit {{eventLabel}}', { eventLabel }), selected: page === pageKeys.EDIT_EVENT, condition: page === pageKeys.EDIT_EVENT, }, { key: pageKeys.NEW_EVENT, onClick: () => undefined, - label: i18n.t('New event'), + label: i18n.t('New {{eventLabel}}', { eventLabel }), selected: page === pageKeys.NEW_EVENT, condition: page === pageKeys.NEW_EVENT, }, @@ -135,6 +140,8 @@ const BreadcrumbsPlain = ({ onBackToMainPage, onBackToDashboard, onBackToViewEvent, + enrollmentLabel, + eventLabel, ]); 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..3ad50545ff 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 { LabelKeys, useTermLabel } from '../../../../metaData'; type Template = { id: string; @@ -22,17 +23,11 @@ const DefaultFilterKeys = { type DefaultFilterKey = typeof DefaultFilterKeys[keyof typeof DefaultFilterKeys]; -const DefaultFilterLabels: { [key in DefaultFilterKey]: string } = { - [DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'), - [DefaultFilterKeys.ACTIVE]: i18n.t('Active enrollments'), - [DefaultFilterKeys.COMPLETE]: i18n.t('Completed enrollments'), - [DefaultFilterKeys.CANCELLED]: i18n.t('Cancelled enrollments'), -}; - export const useWorkingListLabel = ({ programId, displayFrontPageList, }: Props) => { + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural], { programId }); const workingListTemplates = useSelector((state: any) => state.workingListsTemplates?.teiList); const workingListProgramId = useSelector((state: any) => state.workingListsContext?.teiList?.programIdView); @@ -41,6 +36,13 @@ export const useWorkingListLabel = ({ const selectedTemplate: Template | undefined = templates?.find(({ id }) => id === selectedTemplateId); const isSameProgram: boolean = workingListProgramId === programId; + const defaultFilterLabels: { [key in DefaultFilterKey]: string } = useMemo(() => ({ + [DefaultFilterKeys.DEFAULT]: i18n.t('Program overview'), + [DefaultFilterKeys.ACTIVE]: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.COMPLETE]: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), + [DefaultFilterKeys.CANCELLED]: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), + }), [enrollmentsLabel]); + const label: string = useMemo(() => { if (isLoadingTemplates) return i18n.t('Loading...'); @@ -51,7 +53,7 @@ export const useWorkingListLabel = ({ if (selectedTemplateId && !selectedTemplate && DefaultFilterKeys[selectedTemplateId.toUpperCase() as keyof typeof DefaultFilterKeys]) { - return DefaultFilterLabels[selectedTemplateId as DefaultFilterKey]; + return defaultFilterLabels[selectedTemplateId as DefaultFilterKey]; } return i18n.t('Program overview'); @@ -66,6 +68,7 @@ export const useWorkingListLabel = ({ isSameProgram, selectedTemplate, selectedTemplateId, + defaultFilterLabels, ]); return { diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx index b19159a950..09140036ba 100644 --- a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx +++ b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/EventBreadcrumb.tsx @@ -1,5 +1,5 @@ -import React, { ComponentType, useCallback, useMemo, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { ComponentType, useCallback, useMemo, useState } from 'react'; import { colors } from '@dhis2/ui'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { DirectionalChevron } from '../../../utils/rtl'; @@ -7,6 +7,7 @@ import { BreadcrumbItem } from '../common/BreadcrumbItem'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { defaultDialogProps } from '../../Dialogs/DiscardDialog.constants'; import { useWorkingListLabel } from './hooks/useWorkingListLabel'; +import { LabelKeys, useTermLabel } from '../../../metaData'; export const pageKeys = { MAIN_PAGE: 'mainPage', @@ -43,6 +44,7 @@ const EventBreadcrumbPlain = ({ }: Props) => { const [openWarning, setOpenWarning] = useState(null); const { label } = useWorkingListLabel({ programId }); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId }); const handleNavigation = useCallback((callback?: () => void, warningType?: PageKeys) => { if (userInteractionInProgress && warningType) { @@ -71,14 +73,14 @@ const EventBreadcrumbPlain = ({ { key: pageKeys.VIEW_EVENT, onClick: () => handleNavigation(onBackToViewEvent, pageKeys.VIEW_EVENT), - label: i18n.t('View event'), + label: i18n.t('View {{eventLabel}}', { eventLabel }), selected: page === pageKeys.VIEW_EVENT, condition: page === pageKeys.VIEW_EVENT || page === pageKeys.EDIT_EVENT, }, { key: pageKeys.EDIT_EVENT, onClick: () => undefined, - label: i18n.t('Edit event'), + label: i18n.t('Edit {{eventLabel}}', { eventLabel }), selected: page === pageKeys.EDIT_EVENT, condition: page === pageKeys.EDIT_EVENT, }, @@ -88,6 +90,7 @@ const EventBreadcrumbPlain = ({ onBackToViewEvent, onBackToMainPage, page, + eventLabel, ]); return ( diff --git a/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts b/src/core_modules/capture-core/components/Breadcrumbs/EventBreadcrumb/hooks/useWorkingListLabel.ts index 5191a000c9..0daf6f7045 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 { LabelKeys, useTermLabel } from '../../../../metaData'; type Template = { id: string; @@ -15,6 +16,7 @@ type Props = { export const useWorkingListLabel = ({ programId }: Props) => { const workingListTemplate = useSelector((state: any) => state.workingListsTemplates?.eventList); const workingListProgramId = useSelector((state: any) => state.workingListsContext?.eventList?.programIdView); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId }); const { selectedTemplateId, @@ -33,8 +35,8 @@ export const useWorkingListLabel = ({ programId }: Props) => { return selectedTemplete.name; } - return i18n.t('Event list'); - }, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete]); + return i18n.t('{{eventLabel}} list', { eventLabel }); + }, [isDefaultTemplate, isSameProgram, loadingTemplates, selectedTemplete, eventLabel]); 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..57b45be965 100644 --- a/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListButtons.component.tsx @@ -8,6 +8,7 @@ import { availableCardListButtonState, enrollmentTypes } from './CardList.consta import { navigateToEnrollmentOverview, } from '../../actions/navigateToEnrollmentOverview/navigateToEnrollmentOverview.actions'; +import { useTermLabel, LabelKeys } from '../../metaData'; type Props = { currentSearchScopeId?: string, @@ -79,6 +80,7 @@ const CardListButtons: FC = ({ }) => { const dispatch = useDispatch(); const navigationButtonsState: AvailableCardListButtonState = deriveNavigationButtonState(enrollmentType); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular]); const onHandleClick: ButtonEventHandler> = useCallback((_, event) => { event.stopPropagation(); @@ -115,7 +117,7 @@ const CardListButtons: FC = ({ { dataTest: 'view-active-enrollment-button', onClick: onHandleClick, - label: i18n.t('View active enrollment'), + label: i18n.t('View active {{enrollmentLabel}}', { enrollmentLabel }), hide: navigationButtonsState !== availableCardListButtonState.SHOW_VIEW_ACTIVE_ENROLLMENT_BUTTON, }, { diff --git a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx index 54d3ea1ec7..38bdcef13a 100644 --- a/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx +++ b/src/core_modules/capture-core/components/CardList/CardListItem.component.tsx @@ -17,6 +17,8 @@ import { getTrackerProgramThrowIfNotFound, OptionSet, type TrackerProgram, + useTermLabel, + LabelKeys, } from '../../metaData'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import type { ListItem, RenderCustomCardActions } from './CardList.types'; @@ -152,6 +154,10 @@ const CardListItemIndex = ({ const enrollmentType = deriveEnrollmentType(enrollments, currentProgramId); const { orgUnitId, enrolledAt } = deriveEnrollmentOrgUnitIdAndDate(enrollments, enrollmentType, currentProgramId); const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(orgUnitId ?? null); + const { enrollmentLabel, orgUnitLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.orgUnitSingular], + { programId: currentProgramId }, + ); const program: TrackerProgram | undefined = enrollments.length ? deriveProgramFromEnrollment(enrollments, currentSearchScopeType) : undefined; @@ -217,11 +223,14 @@ const CardListItemIndex = ({ return (<> diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx index 81802aa393..c2fd673dbe 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentDataEntry.component.tsx @@ -33,7 +33,14 @@ import { getIncidentDateValidatorContainer, } from './fieldValidators'; import { sectionKeysForEnrollmentDataEntry } from './constants/sectionKeys.const'; -import { type Enrollment, ProgramStage, RenderFoundation, getProgramThrowIfNotFound } from '../../../metaData'; +import { + type Enrollment, + ProgramStage, + RenderFoundation, + getProgramThrowIfNotFound, + getTermLabel, + LabelKeys, +} from '../../../metaData'; import { EnrollmentWithFirstStageDataEntry } from './EnrollmentWithFirstStageDataEntry'; import { getCategoryOptionsValidatorContainers, @@ -362,30 +369,32 @@ class FinalEnrollmentDataEntry extends React.Component { inMemoryFileStore.clear(); } - static dataEntrySectionDefinitions = { - [sectionKeysForEnrollmentDataEntry.ENROLLMENT]: { - placement: placements.TOP, - name: i18n.t('Enrollment'), - }, - [AOCsectionKey]: { - placement: placements.BOTTOM, - }, - }; - render() { - const { enrollmentMetadata, firstStageMetaData, relatedStageActionsOptions, ...passOnProps } = this.props; + const { enrollmentMetadata, firstStageMetaData, relatedStageActionsOptions, programId, ...passOnProps } = this.props; + + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId }); + const dataEntrySections = { + [sectionKeysForEnrollmentDataEntry.ENROLLMENT]: { + placement: placements.TOP, + name: enrollmentLabel, + }, + [AOCsectionKey]: { + placement: placements.BOTTOM, + }, + }; return ( firstStageMetaData ? ( ) : ( ) ); diff --git a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx index 620d33dd1d..5e626b6906 100644 --- a/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/Enrollment/EnrollmentWithFirstStageDataEntry/EnrollmentWithFirstStageDataEntry.component.tsx @@ -1,5 +1,6 @@ import i18n from '@dhis2/d2-i18n'; import { isLangRtl } from '../../../../utils/rtl'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; import { DataEntry } from '../../../DataEntry'; import { Assignee } from '../../SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/Assignee'; import { @@ -176,10 +177,15 @@ const getCompleteFieldSettingsFn = () => { const completeSettings = { isApplicable: (props: any) => props.firstStageMetaData && props.firstStageMetaData.stage?.stageForm, getComponent: () => completeComponent, - getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Complete event'), - id: 'complete', - }), + getComponentProps: (props: any) => { + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: props.programId }); + return createComponentProps(props, { + label: i18n.t('Complete {{eventLabel}}', { + eventLabel, + }), + id: 'complete', + }); + }, getPropName: () => stageMainDataIds.COMPLETE, getValidatorContainers: () => [], getMeta: () => ({ 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..6fe5e54c25 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 { LabelKeys, useTermLabel } from '../../../../../metaData'; -export const useDataEntrySections = (stageName: string, beforeSectionId: string) => - useMemo( +export const useDataEntrySections = (stageName: string, beforeSectionId: string) => { + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular]); + return useMemo( () => ({ [sectionKeysForFirstStageDataEntry.ENROLLMENT]: { placement: placements.TOP, - name: i18n.t('Enrollment'), + name: enrollmentLabel, }, [sectionKeysForFirstStageDataEntry.STAGE_BASIC_INFO]: { beforeSectionId, @@ -35,5 +37,6 @@ export const useDataEntrySections = (stageName: string, beforeSectionId: string) }), }, }), - [stageName, beforeSectionId], + [stageName, beforeSectionId, enrollmentLabel], ); +}; diff --git a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx index 19b8892892..8ac1045798 100644 --- a/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/EnrollmentRegistrationEntry/EnrollmentRegistrationEntry.component.tsx @@ -4,7 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { compose } from 'redux'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; -import { scopeTypes } from '../../../metaData'; +import { LabelKeys, scopeTypes, useTermLabel } from '../../../metaData'; import { DiscardDialog } from '../../Dialogs/DiscardDialog.component'; import { EnrollmentDataEntry } from '../Enrollment'; import type { Props, PlainProps } from './EnrollmentRegistrationEntry.types'; @@ -26,13 +26,14 @@ const translatedTextWithStylesForProgram = ( trackedEntityName: string, programName: string, orgUnitName: string, + enrollmentLabel: string, teiId?: string, ) => ( teiId ? - {i18n.t('Saving a new enrollment in {{programName}} in {{orgUnitName}}.', { + {i18n.t('Saving a new {{enrollmentLabel}} in {{programName}} in {{orgUnitName}}.', { + enrollmentLabel, programName, orgUnitName, - interpolation: { escapeValue: false }, })} : {i18n.t('Saving a {{trackedEntityName}} in {{programName}} in {{orgUnitName}}.', { @@ -65,6 +66,7 @@ const EnrollmentRegistrationEntryPlain = }: PlainProps & WithStyles) => { const [showWarning, setShowWarning] = useState(false); const { scopeType, trackedEntityName, programName } = useScopeInfo(selectedScopeId); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular]); const handleOnCancel = () => { if (!isUserInteractionInProgress) { @@ -122,6 +124,7 @@ const EnrollmentRegistrationEntryPlain = trackedEntityName.toLowerCase(), programName, orgUnit.name, + enrollmentLabel, teiId, )} diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx index a60123b6fe..c4eb1e23b1 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.component.tsx @@ -213,11 +213,11 @@ const buildOrgUnitSettingsFn = () => { getComponent: () => orgUnitComponent, getComponentProps: (props: any) => createComponentProps(props, { width: props && props.formHorizontal ? 150 : 350, - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, required: true, }), getPropName: () => 'orgUnit', - getValidatorContainers: () => getOrgUnitValidatorContainers(), + getValidatorContainers: (props: any) => getOrgUnitValidatorContainers(props.orgUnitLabel), getMeta: () => ({ placement: placements.TOP, section: dataEntrySectionNames.BASICINFO, @@ -328,7 +328,7 @@ const buildCompleteFieldSettingsFn = () => { const completeSettings = { getComponent: () => completeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Complete event'), + label: i18n.t('Complete {{eventLabel}}', { eventLabel: props.eventLabel }), id: 'complete', }), getPropName: () => 'complete', @@ -402,13 +402,14 @@ const buildNotesSettingsFn = () => { const notesSettings = { getComponent: () => noteComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Notes'), + label: props.notesLabel, onAddNote: props.onAddNote, id: 'notes', dataEntryId: props.id, + noteLabel: props.noteLabel, }), getPropName: () => 'note', - getValidatorContainers: () => getNoteValidatorContainers(), + getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel, props.noteLabel), getMeta: () => ({ placement: placements.BOTTOM, section: dataEntrySectionNames.NOTES, @@ -541,47 +542,21 @@ type Props = { formHorizontal: boolean | null, recentlyAddedRelationshipId?: string | null, onScrollToRelationships: () => void; + notesLabel: string, + relationshipsLabel: string, }; type DataEntrySection = { placement: typeof placements[keyof typeof placements], name?: string, }; -const dataEntrySectionDefinitions = { - [dataEntrySectionNames.BASICINFO]: { - placement: placements.TOP, - name: i18n.t('Basic info'), - }, - [AOCsectionKey]: { - placement: placements.TOP, - }, - [dataEntrySectionNames.STATUS]: { - placement: placements.BOTTOM, - name: i18n.t('Status'), - }, - [dataEntrySectionNames.NOTES]: { - placement: placements.BOTTOM, - name: i18n.t('Notes'), - }, - [dataEntrySectionNames.RELATIONSHIPS]: { - placement: placements.BOTTOM, - name: i18n.t('Relationships'), - }, - [dataEntrySectionNames.ASSIGNEE]: { - placement: placements.BOTTOM, - name: i18n.t('Assignee'), - }, -}; - class NewEventDataEntry extends Component> { fieldOptions: { theme: any }; - dataEntrySections: { [key: string]: DataEntrySection }; constructor(props: Props & WithStyles) { super(props); this.fieldOptions = { theme: props.theme, }; - this.dataEntrySections = dataEntrySectionDefinitions; } componentDidMount() { @@ -656,8 +631,35 @@ class NewEventDataEntry extends Component> onSetSaveTypes, onSaveAndAddAnother, theme, + notesLabel, + relationshipsLabel, ...passOnProps } = this.props; + const dataEntrySections: { [key: string]: DataEntrySection } = { + [dataEntrySectionNames.BASICINFO]: { + placement: placements.TOP, + name: i18n.t('Basic info'), + }, + [AOCsectionKey]: { + placement: placements.TOP, + }, + [dataEntrySectionNames.STATUS]: { + placement: placements.BOTTOM, + name: i18n.t('Status'), + }, + [dataEntrySectionNames.NOTES]: { + placement: placements.BOTTOM, + name: notesLabel, + }, + [dataEntrySectionNames.RELATIONSHIPS]: { + placement: placements.BOTTOM, + name: relationshipsLabel, + }, + [dataEntrySectionNames.ASSIGNEE]: { + placement: placements.BOTTOM, + name: i18n.t('Assignee'), + }, + }; return (
@@ -669,8 +671,10 @@ class NewEventDataEntry extends Component> selectedOrgUnitId={orgUnit?.id} onSave={this.handleSave} fieldOptions={this.fieldOptions} - dataEntrySections={this.dataEntrySections} + dataEntrySections={dataEntrySections} relationshipsRef={this.setRelationshipsInstance} + notesLabel={notesLabel} + relationshipsLabel={relationshipsLabel} orgUnit={orgUnit} // @ts-expect-error - keeping original functionality as before ts rewrite orgUnitId={orgUnit?.id} diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts index 6248331077..6a4729219a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/DataEntry.container.ts @@ -22,9 +22,18 @@ import { import { makeProgramNameSelector, } from './dataEntry.selectors'; -import type { RenderFoundation } from '../../../../../metaData'; +import { LabelKeys, type RenderFoundation } from '../../../../../metaData'; import { withLoadingIndicator, withErrorMessageHandler } from '../../../../../HOC'; import { newEventSaveTypes } from './newEventSaveTypes'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; + +const customLabels = [ + LabelKeys.orgUnitSingular, + LabelKeys.eventSingular, + LabelKeys.noteSingular, + LabelKeys.notePlural, + LabelKeys.relationshipPlural, +] as const; const makeMapStateToProps = () => { const programNameSelector = makeProgramNameSelector(); @@ -111,5 +120,7 @@ const mapDispatchToProps = (dispatch: any) => ({ }); export const DataEntry = connect(makeMapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(withErrorMessageHandler()(DataEntryComponent)), + withLoadingIndicator()(withErrorMessageHandler()( + withCustomLabels(customLabels)(DataEntryComponent), + )), ); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts index bd76dee431..3838a93553 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/actions/dataEntry.actions.ts @@ -58,6 +58,7 @@ export const newEventSavedAfterReturnedToMainPage = (selections: any) => export const startSaveNewEventAfterReturnedToMainPage = (serverData: any, relationshipData: any, selections: any) => { const actionType = actionTypes.START_SAVE_AFTER_RETURNED_TO_MAIN_PAGE; + const programId = serverData?.events?.[0]?.program; return actionCreator(actionType)({ selections }, { offline: { effect: { @@ -69,7 +70,10 @@ export const startSaveNewEventAfterReturnedToMainPage = (serverData: any, relati type: actionTypes.SAVE_NEW_EVENT_RELATIONSHIPS_IF_EXISTS, meta: { selections, relationshipData, triggerAction: actionType }, }, - rollback: { type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_AFTER_RETURNED_TO_MAIN_PAGE, meta: { selections } }, + rollback: { + type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_AFTER_RETURNED_TO_MAIN_PAGE, + meta: { selections, programId }, + }, }, }); }; @@ -156,6 +160,7 @@ export const startSaveNewEventAddAnother = clientId: string, ) => { const actionType = actionTypes.START_SAVE_NEW_EVENT_ADD_ANOTHER; + const programId = serverData?.events?.[0]?.program; return actionCreator(actionTypes.START_SAVE_NEW_EVENT_ADD_ANOTHER)({ selections }, { offline: { effect: { @@ -168,7 +173,10 @@ export const startSaveNewEventAddAnother = type: actionTypes.SAVE_NEW_EVENT_RELATIONSHIPS_IF_EXISTS, meta: { selections, relationshipData, triggerAction: actionType }, }, - rollback: { type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_ADD_ANOTHER, meta: { selections, clientId } }, + rollback: { + type: actionTypes.SAVE_FAILED_FOR_NEW_EVENT_ADD_ANOTHER, + meta: { selections, clientId, programId }, + }, }, }); }; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts index 3d71ca94aa..26c8e2a786 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/epics/addRelationshipForNewSingleEvent.epics.ts @@ -1,10 +1,10 @@ +import i18n from '@dhis2/d2-i18n'; import uuid from 'd2-utilizr/lib/uuid'; import { ofType } from 'redux-observable'; import { map } from 'rxjs/operators'; -import i18n from '@dhis2/d2-i18n'; import { batchActions } from 'redux-batched-actions'; import type { EpicAction, ReduxStore } from 'capture-core-utils/types'; - +import { getTermLabel, LabelKeys } from '../../../../../../metaData'; import { initializeNewRelationship, } from '../../../../../Pages/NewRelationship/newRelationship.actions'; @@ -76,11 +76,13 @@ export const addRelationshipForNewSingleEventEpic = (action$: EpicAction, store: R displayName: category.name, })), } : null; - return batchActions(getOpenDataEntryActions(programCategory, selectedCategories, orgUnit)); + return batchActions(getOpenDataEntryActions( + program.id, + programCategory, + selectedCategories, + orgUnit, + )); }), ); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts index f029ddcd1b..9991199b93 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/note.validatorContainersGetter.ts @@ -2,12 +2,12 @@ import i18n from '@dhis2/d2-i18n'; const validateNote = (value?: string) => !value; -export const getNoteValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateNote, - errorMessage: i18n.t('Please add or cancel the note before saving the event'), - }, - ]; - return validatorContainers; -}; +export const getNoteValidatorContainers = (eventLabel: string, noteLabel: string) => [ + { + validator: validateNote, + errorMessage: i18n.t('Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}', { + eventLabel, + noteLabel, + }), + }, +]; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts index ec6fb14c72..4deebd3128 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts @@ -1,14 +1,11 @@ -import { isValidOrgUnit } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; +import { isValidOrgUnit } from 'capture-core-utils/validators/form'; const validateOrgUnit = (value?: any) => isValidOrgUnit(value); -export const getOrgUnitValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateOrgUnit, - errorMessage: i18n.t('Please provide an valid organisation unit'), - }, - ]; - return validatorContainers; -}; +export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ + { + validator: validateOrgUnit, + errorMessage: i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + }, +]; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts index acf0dffa97..9e027b95df 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/helpers/getOpenDataEntryActions.ts @@ -8,8 +8,13 @@ import { addFormData } from '../../../../../D2Form/actions/form.actions'; import { getCategoryOptionsValidatorContainers } from '../../../../Enrollment/fieldValidators'; import type { ProgramCategory } from '../../../../../WidgetEventSchedule/CategoryOptions/CategoryOptions.types'; import type { DataEntryPropToInclude } from '../../../../../DataEntry/actions/dataEntryLoad.utils'; +import { getTermLabel, LabelKeys } from '../../../../../../metaData'; -const dataEntryPropsToInclude: Array = [ +const buildDataEntryPropsToInclude = ( + orgUnitLabel: string, + eventLabel: string, + noteLabel: string, +): Array => [ { id: 'occurredAt', type: 'DATE', @@ -18,7 +23,7 @@ const dataEntryPropsToInclude: Array = [ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(orgUnitLabel), }, { clientId: 'geometry', @@ -28,7 +33,7 @@ const dataEntryPropsToInclude: Array = [ { id: 'note', type: 'TEXT', - validatorContainers: getNoteValidatorContainers(), + validatorContainers: getNoteValidatorContainers(eventLabel, noteLabel), clientIgnore: true, }, { @@ -49,6 +54,7 @@ const dataEntryPropsToInclude: Array = [ ]; export const getOpenDataEntryActions = ( + programId: string, programCategory?: ProgramCategory | null, selectedCategories?: { [key: string]: string } | null, orgUnit?: CoreOrgUnit | null, @@ -59,6 +65,15 @@ export const getOpenDataEntryActions = ( : undefined, }; + const { orgUnitLabel, eventLabel, noteLabel } = getTermLabel( + [LabelKeys.orgUnitSingular, LabelKeys.eventSingular, LabelKeys.noteSingular], + { programId }, + ); + const dataEntryPropsToInclude = buildDataEntryPropsToInclude( + orgUnitLabel, + eventLabel, + noteLabel, + ); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ id: `attributeCategoryOptions-${category.id}`, diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/RecentlyAddedEventsList/RecentlyAddedEventsList.component.tsx index 06697c00e2..c9e65add46 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 @@ -1,11 +1,12 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Card } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { OfflineEventsList } from '../../../../EventsList/OfflineEventsList/OfflineEventsList.component'; import { listId } from './RecentlyAddedEventsList.const'; import type { Props } from './RecentlyAddedEventsList.types'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; const styles = (theme: any) => ({ container: { @@ -18,6 +19,9 @@ const styles = (theme: any) => ({ const NewEventsListPlain = (props: Props & WithStyles) => { const { classes, ...passOnProps } = props; + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + ); const eventsAdded = props.events ? Object.keys(props.events).length : 0; if (eventsAdded === 0) { return null; @@ -27,16 +31,18 @@ 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, + eventsLabel, + defaultValue: '{{count}} {{eventLabel}} added', + defaultValue_plural: '{{count}} {{eventsLabel}} added', })}
diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx index 37c83bfbfe..a7cb5978d5 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.component.tsx @@ -5,6 +5,7 @@ import { withStyles, WithStyles } from 'capture-core-utils/styles'; import { NewRelationship } from '../../../Pages/NewRelationship/NewRelationship.container'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; const getStyles = (theme: any) => ({ headerContainer: { @@ -43,6 +44,7 @@ type Props = { onCancel: (dataEntryid: string) => void; dataEntryKey: string; unsavedRelationships: any; + programId: string; }; type State = { @@ -79,32 +81,37 @@ class NewEventNewRelationshipWrapper extends React.Component ( -
-
- {i18n.t('New event relationship')} -
-
- ); - render() { - const { classes, onCancel, ...passOnProps } = this.props; + const { classes, onCancel, programId, ...passOnProps } = this.props; + const { eventLabel, relationshipLabel } = getTermLabel( + [LabelKeys.eventSingular, LabelKeys.relationshipSingular], + { programId }, + ); return (
- {i18n.t('Adding relationship to event.')} + + {i18n.t( + 'Adding {{relationshipLabel}} to {{eventLabel}}.', + { eventLabel, relationshipLabel }, + )} + - {i18n.t('Go back to event without saving relationship')} + {i18n.t( + 'Go back to {{eventLabel}} without saving {{relationshipLabel}}', + { eventLabel, relationshipLabel }, + )}
this.props.onCancel('relationship')} diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.container.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.container.tsx index f8a70bec60..cd2488c2f6 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.container.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/NewRelationshipWrapper/NewEventNewRelationshipWrapper.container.tsx @@ -16,6 +16,7 @@ const makeMapStateToProps = () => { return { relationshipTypes, unsavedRelationships, + programId: state.currentSelections.programId, }; }; diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx index 6843b28f2a..4012faf143 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.component.tsx @@ -1,19 +1,24 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { useDispatch } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; import { NewEventDataEntryWrapper } from './DataEntryWrapper/NewEventDataEntryWrapper.container'; import { NewRelationshipWrapper } from './NewRelationshipWrapper/NewEventNewRelationshipWrapper.container'; import { cancelNewEventAndReturnToMainPage } from './DataEntryWrapper/DataEntry/actions/dataEntry.actions'; import type { Props } from './SingleEventRegistrationEntry.types'; +import { LabelKeys, useTermLabel } from '../../../metaData'; export const SingleEventRegistrationEntryComponent = ({ showAddRelationship, eventAccess }: Props) => { const dispatch = useDispatch(); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular]); if (!eventAccess.write) { return ( dispatch(cancelNewEventAndReturnToMainPage())} /> ); diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx index baffeaec84..fe557f923a 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/SingleEventRegistrationEntry.container.tsx @@ -44,12 +44,12 @@ const openSingleEventDataEntry = (InnerComponent: React.ComponentType (<> {i18n.t('Saving a {{trackedEntityName}}', { trackedEntityName, interpolation: { escapeValue: false } }) - } {i18n.t('without')} {i18n.t('enrollment')} + } {i18n.t('without')} {enrollmentLabel} {orgUnitName && <>{' '}{i18n.t('in')} {orgUnitName}}.{' '} {!hideProgramSelectionMessage && i18n.t('Enroll in a program by selecting a program from the top bar.')} ); @@ -59,6 +60,7 @@ const TeiRegistrationEntryPlain = const { formId, formFoundation } = useMetadataForRegistrationForm({ selectedScopeId }); const orgUnitId = useCurrentOrgUnitId(); const { displayName: orgUnitName } = useOrgUnitNameWithAncestors(orgUnitId); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular]); const handleOnCancel = () => { if (!isUserInteractionInProgress) { @@ -111,7 +113,7 @@ const TeiRegistrationEntryPlain =
{translatedTextWithStylesForTei( - trackedEntityName.toLowerCase(), orgUnitName, hideProgramSelectionMessage, + trackedEntityName.toLowerCase(), enrollmentLabel, orgUnitName, hideProgramSelectionMessage, )} diff --git a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx index 08e3dd866a..54b1b8a08e 100644 --- a/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx +++ b/src/core_modules/capture-core/components/DataEntries/common/trackerEvent/withAskToCompleteEnrollment/CompleteModal/CompleteModal.component.tsx @@ -2,6 +2,7 @@ import { Modal, ModalActions, ModalContent, ModalTitle, Button, ButtonStrip } fr import React from 'react'; import i18n from '@dhis2/d2-i18n'; import type { PlainProps, PlainPropsWithEvents } from './completeModal.types'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; export const CompleteEnrollmentAndEventsModalComponent = ({ programStageName, @@ -10,95 +11,114 @@ export const CompleteEnrollmentAndEventsModalComponent = ({ onCancel, onCompleteEnrollmentAndEvents, onCompleteEnrollment, -}: PlainPropsWithEvents) => ( - - - {i18n.t('{{programStageName}} completed', { - programStageName, - interpolation: { escapeValue: false }, - })} - - -

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

+}: PlainPropsWithEvents) => { + const { enrollmentLabel, eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.eventSingular, LabelKeys.eventPlural], + ); + return ( + + + {i18n.t('{{programStageName}} completed', { + programStageName, + interpolation: { escapeValue: false }, + })} + + +

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

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

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

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

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

+ + + + + + +
+
+ ); +}; diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.tsx b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.tsx index 415484ad83..a77ba24587 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.tsx +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withFeedbackOutput.tsx @@ -1,11 +1,12 @@ +import i18n from '@dhis2/d2-i18n'; import * as React from 'react'; import { connect } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; import { useHideWidgetByRuleLocations } from 'capture-core/hooks'; import { getDataEntryKey } from '../common/getDataEntryKey'; import { withDataEntryOutput } from './withDataEntryOutput'; import { WidgetFeedback } from '../../WidgetFeedback'; import { makeProgramRulesSelector } from './dataEntryOutput.selectors'; +import { LabelKeys, useTermLabel } from '../../../metaData'; type Props = { dataEntryKey?: string; @@ -14,6 +15,7 @@ type Props = { const FeedbackOutputWrapper = (props: Props) => { const { dataEntryKey, programRules } = props; + const { eventLabel } = useTermLabel([LabelKeys.eventSingular]); const hideWidgets = useHideWidgetByRuleLocations(programRules || []); @@ -24,7 +26,7 @@ const FeedbackOutputWrapper = (props: Props) => { return ( ); }; diff --git a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.tsx b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.tsx index 7a8284a2ef..bbbb576a33 100644 --- a/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.tsx +++ b/src/core_modules/capture-core/components/DataEntry/dataEntryOutput/withIndicatorOutput.tsx @@ -1,11 +1,12 @@ +import i18n from '@dhis2/d2-i18n'; import * as React from 'react'; import { connect } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; import { useHideWidgetByRuleLocations } from '../../../hooks'; import { getDataEntryKey } from '../common/getDataEntryKey'; import { withDataEntryOutput } from './withDataEntryOutput'; import { WidgetIndicator } from '../../WidgetIndicator'; import { makeProgramRulesSelector } from './dataEntryOutput.selectors'; +import { LabelKeys, useTermLabel } from '../../../metaData'; type Props = { dataEntryKey?: string; @@ -14,6 +15,7 @@ type Props = { const IndicatorOutputWrapper = (props: Props) => { const { dataEntryKey, programRules } = props; + const { eventLabel } = useTermLabel([LabelKeys.eventSingular]); const hideWidgets = useHideWidgetByRuleLocations(programRules || []); @@ -24,7 +26,7 @@ const IndicatorOutputWrapper = (props: Props) => { return ( ); }; diff --git a/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx b/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx index 14cf03660f..d1618a1c68 100644 --- a/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx +++ b/src/core_modules/capture-core/components/DataEntry/withAskToCreateNew.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button } from '@dhis2/ui'; import type { RenderFoundation } from '../../metaData'; +import { getTermLabel, LabelKeys } from '../../metaData'; import { addEventSaveTypes } from '../WidgetEnrollmentEventNew/DataEntry/addEventSaveTypes'; type Props = { @@ -13,6 +14,7 @@ type Props = { availableProgramStages?: Array>; isCompleted?: boolean; itemId: string; + programId: string; }; type State = { @@ -51,17 +53,17 @@ const askToCreateNewComponent = (InnerComponent: React.ComponentType) => if (!this.state.isOpen) { return null; } - + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: this.props.programId }); return ( - {i18n.t('Generate new event')} + {i18n.t('Generate new {{eventLabel}}', { eventLabel })} - {i18n.t('Do you want to create another event?')} + {i18n.t('Do you want to create another {{eventLabel}}?', { eventLabel })} @@ -81,7 +83,7 @@ const askToCreateNewComponent = (InnerComponent: React.ComponentType) => }} primary > - {i18n.t('Yes, create new event')} + {i18n.t('Yes, create new {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts b/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts index bca310bb95..da03ddadf1 100644 --- a/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts +++ b/src/core_modules/capture-core/components/DataEntryWidgetOutput/DataEntryWidgetOutput.container.ts @@ -1,9 +1,10 @@ +import i18n from '@dhis2/d2-i18n'; import { connect } from 'react-redux'; import React, { type ComponentType } from 'react'; -import i18n from '@dhis2/d2-i18n'; import { DataEntryWidgetOutputComponent } from './DataEntryWidgetOutput.component'; import { getDataEntryKey } from '../DataEntry/common/getDataEntryKey'; import { makeProgramRulesSelector } from './DataEntryWidgetOutput.selectors'; +import { getTermLabel, LabelKeys } from '../../metaData'; type OwnProps = { dataEntryId: string; @@ -17,18 +18,26 @@ const makeMapStateToProps = () => { const { dataEntries } = state; const ready = !!dataEntries[dataEntryId]; const dataEntryKey = ready ? getDataEntryKey(dataEntryId, state.dataEntries[dataEntryId].itemId) : null; + // Example use of getTermLabel. + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId: selectedScopeId }); return { ready, dataEntryKey, programRules: programRulesSelector(state, { dataEntryId, selectedScopeId }), - feedbackEmptyText: i18n.t('No feedback for this enrollment yet'), - indicatorEmptyText: i18n.t('No indicator output for this enrollment yet'), + feedbackEmptyText: i18n.t( + 'No feedback for this {{enrollmentLabel}} yet', + { enrollmentLabel }, + ), + indicatorEmptyText: i18n.t( + 'No indicator output for this {{enrollmentLabel}} yet', + { enrollmentLabel }, + ), }; }; }; export const DataEntryWidgetOutput: ComponentType = - connect(makeMapStateToProps, () => ({}))( - (props: any) => (props.ready ? React.createElement(DataEntryWidgetOutputComponent, props) : null), - ); + connect(makeMapStateToProps, () => ({}))( + (props: any) => (props.ready ? React.createElement(DataEntryWidgetOutputComponent, props) : null), + ); diff --git a/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts b/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts index d7e6cb3f24..56483f54ed 100644 --- a/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts +++ b/src/core_modules/capture-core/components/Dialogs/DiscardDialog.constants.ts @@ -2,8 +2,10 @@ import i18n from '@dhis2/d2-i18n'; export const defaultDialogProps = { header: i18n.t('Discard unsaved changes?'), - // eslint-disable-next-line max-len - text: i18n.t('This event has unsaved changes. Leaving this page without saving will lose these changes. Are you sure you want to discard unsaved changes?'), + text: i18n.t( + 'This form has unsaved changes. Leaving this page without saving will lose these changes. ' + + 'Are you sure you want to discard unsaved changes?', + ), destructiveText: i18n.t('Yes, discard changes'), cancelText: i18n.t('No, cancel'), }; diff --git a/src/core_modules/capture-core/components/EventsList/OfflineEventsList/OfflineEventsList.component.tsx b/src/core_modules/capture-core/components/EventsList/OfflineEventsList/OfflineEventsList.component.tsx index 2f9ccba673..dd3bfdbc66 100644 --- a/src/core_modules/capture-core/components/EventsList/OfflineEventsList/OfflineEventsList.component.tsx +++ b/src/core_modules/capture-core/components/EventsList/OfflineEventsList/OfflineEventsList.component.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; import { OfflineListWrapper } from './OfflineListWrapper.container'; type Props = { @@ -16,8 +15,8 @@ export class OfflineEventsList extends React.Component { return ( ); diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/OrgUnitField/SingleOrgUnitSelectField.component.tsx b/src/core_modules/capture-core/components/FormFields/New/Fields/OrgUnitField/SingleOrgUnitSelectField.component.tsx index 7a2709bc4b..168220d3af 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/OrgUnitField/SingleOrgUnitSelectField.component.tsx +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/OrgUnitField/SingleOrgUnitSelectField.component.tsx @@ -1,13 +1,17 @@ -import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; +import * as React from 'react'; import { cx } from '@emotion/css'; import { debounce } from 'lodash'; import { v4 as uuid } from 'uuid'; import { Chip, Popover, IconChevronDown16, colors } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { withCustomLabels } from '../../../../../HOC/withCustomLabels'; import { OrgUnitField } from './OrgUnitField.component'; import { TooltipOrgUnit } from '../../../../Tooltips/TooltipOrgUnit/TooltipOrgUnit.component'; import { useOrgUnitAutoSelect, type AutoSelectOrgUnit } from '../../../../../dataQueries'; +import { LabelKeys } from '../../../../../metaData'; + +const customLabels = [LabelKeys.orgUnitSingular] as const; const getStyles = () => ({ selectedOrgUnitContainer: { @@ -107,6 +111,7 @@ type SingleOrgUnitSelectFieldProps = { disabled?: boolean; maxTreeHeight?: number; autoSelectSingleOrgUnit?: boolean; + orgUnitLabel: string; }; type Props = SingleOrgUnitSelectFieldProps & WithStyles; @@ -284,7 +289,9 @@ class SingleOrgUnitSelectFieldPlain extends React.Component) => ( - <> -
- !item.additionalColumn)} - filtersOnly={filtersOnly} - additionalFilters={additionalFilters} - onUpdateFilter={onUpdateFilter} - onClearFilter={onClearFilter} - onSelectRestMenuItem={onSelectRestMenuItem} - stickyFilters={stickyFilters} - /> -
- {shouldRenderAdditionalFiltersButtons && ( - <> -
-
-
{i18n.t('Stage filters').toUpperCase()}
+}: Props & WithStyles) => { + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular]); + return ( + <> +
+ !item.additionalColumn)} + filtersOnly={filtersOnly} + additionalFilters={additionalFilters} + onUpdateFilter={onUpdateFilter} + onClearFilter={onClearFilter} + onSelectRestMenuItem={onSelectRestMenuItem} + stickyFilters={stickyFilters} + /> +
+ {shouldRenderAdditionalFiltersButtons && ( + <>
- item.additionalColumn)} - filtersOnly={additionalFilters} - onUpdateFilter={onUpdateFilter} - onClearFilter={onClearFilter} - onSelectRestMenuItem={onSelectRestMenuItem} - stickyFilters={stickyFilters} - visibleSelectorId={visibleSelectorId} - onRemoveFilter={itemId => onRemoveFilter(itemId, stickyFilters.filtersWithValueOnInit)} - /> -
- - )} - -); +
+
+ {i18n.t('{{programStageLabel}} filters', { programStageLabel }).toUpperCase()} +
+
+ item.additionalColumn)} + filtersOnly={additionalFilters} + onUpdateFilter={onUpdateFilter} + onClearFilter={onClearFilter} + onSelectRestMenuItem={onSelectRestMenuItem} + stickyFilters={stickyFilters} + visibleSelectorId={visibleSelectorId} + onRemoveFilter={itemId => onRemoveFilter(itemId, stickyFilters.filtersWithValueOnInit)} + /> +
+ + )} + + ); +}; export const FiltersRowsComponent = withStyles(getStyles)(FiltersRowsPlain); diff --git a/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts b/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts index 7d39d4ad67..b96e87a8b5 100644 --- a/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts +++ b/src/core_modules/capture-core/components/LockedSelector/LockedSelector.epics.ts @@ -17,6 +17,7 @@ import { programCollection } from '../../metaDataMemoryStores'; import { getLocationPathname, pageFetchesOrgUnitUsingTheOldWay } from '../../utils/url'; import { getLocationQuery } from '../../utils/routing'; import { getCoreOrgUnit } from '../../metadataRetrieval/coreOrgUnit'; +import { getTermLabel, LabelKeys } from '../../metaData'; export const getOrgUnitDataBasedOnUrlUpdateEpic = (action$: EpicAction, store: ReduxStore) => action$.pipe( @@ -24,14 +25,17 @@ export const getOrgUnitDataBasedOnUrlUpdateEpic = (action$: EpicAction, sto filter(action => action.payload.nextProps.orgUnitId), concatMap((action) => { const { organisationUnits } = store.value as any; - const { orgUnitId } = action.payload.nextProps; + const { orgUnitId, programId } = action.payload.nextProps; if (organisationUnits[orgUnitId]) { return of(completeUrlUpdate()); } + const { orgUnitLabel } = getTermLabel([LabelKeys.orgUnitSingular], { programId }); return of(startLoading(), getCoreOrgUnit({ orgUnitId, onSuccess: setCurrentOrgUnitBasedOnUrl, - onError: () => errorRetrievingOrgUnitBasedOnUrl(i18n.t('Could not get organisation unit')), + onError: () => errorRetrievingOrgUnitBasedOnUrl( + i18n.t('Could not get {{orgUnitLabel}}', { orgUnitLabel }), + ), })); }), ); @@ -63,7 +67,10 @@ export const validateSelectionsBasedOnUrlUpdateEpic = (action$: EpicAction) } if (orgUnitId && !program.organisationUnits[orgUnitId]) { - return invalidSelectionsFromUrl(i18n.t('Selected program is invalid for selected organisation unit')); + const { orgUnitLabel } = getTermLabel([LabelKeys.orgUnitSingular], { programId }); + return invalidSelectionsFromUrl( + i18n.t('Selected program is invalid for selected {{orgUnitLabel}}', { orgUnitLabel }), + ); } } diff --git a/src/core_modules/capture-core/components/Notes/Notes.component.tsx b/src/core_modules/capture-core/components/Notes/Notes.component.tsx index b5b9d305d8..da034f1813 100644 --- a/src/core_modules/capture-core/components/Notes/Notes.component.tsx +++ b/src/core_modules/capture-core/components/Notes/Notes.component.tsx @@ -69,6 +69,7 @@ type Props = { value: string | null; readOnly?: boolean; smallMainButton?: boolean; + noteLabel: string; }; type NotesProps = Props & WithStyles; @@ -80,6 +81,7 @@ const NotesPlain = ({ value: propValue, readOnly = false, smallMainButton, + noteLabel, classes, }: NotesProps) => { const [addIsOpen, setAddIsOpen] = useState(false); @@ -134,7 +136,7 @@ const NotesPlain = ({ className={classes.addNoteContainer} small > - {i18n.t('Add note')} + {i18n.t('Add {{noteLabel}}', { noteLabel })}
); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentPageDefault.container.tsx index bfa61fbf85..9d2f9cbb55 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 @@ -1,11 +1,12 @@ -import React, { useCallback } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { useCallback } from 'react'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; 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 { LabelKeys, useTermLabel } from '../../../../metaData'; import { commitEnrollmentAndEvents, EnrollmentAccessProvider, @@ -53,6 +54,7 @@ export const EnrollmentPageDefault = () => { const { fromClientDate } = useTimeZoneConversion(); const { status: widgetEnrollmentStatus } = useSelector(({ widgetEnrollment }: any) => widgetEnrollment); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular], { programId }); const { orgUnit, error } = useCoreOrgUnit(orgUnitId); const { onLinkedRecordClick } = useLinkedRecordClick(); const { @@ -233,8 +235,14 @@ export const EnrollmentPageDefault = () => { ruleEffects={ruleEffects} widgetEnrollmentStatus={widgetEnrollmentStatus} onAccessLostFromTransfer={onAccessLostFromTransfer} - feedbackEmptyText={i18n.t('No feedback for this enrollment yet')} - indicatorEmptyText={i18n.t('No indicator output for this enrollment yet')} + feedbackEmptyText={i18n.t( + 'No feedback for this {{enrollmentLabel}} yet', + { enrollmentLabel }, + )} + indicatorEmptyText={i18n.t( + 'No indicator output for this {{enrollmentLabel}} yet', + { enrollmentLabel }, + )} /> ); diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx index 6bb4b27e7d..a7374e35f6 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/EnrollmentQuickActions.component.tsx @@ -9,6 +9,7 @@ import { tabMode } from '../../../EnrollmentAddEvent/NewEventWorkspace/newEventW import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../../../utils/routing'; import { useEnrollmentAccessContext } from '../../../common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { OwnProps, ProgramStage, EventCount } from './EnrollmentQuickActions.types'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; const styles = { contentContainer: { @@ -30,6 +31,7 @@ const EnrollmentQuickActionsComponentPlain = ({ const { navigate } = useNavigate(); const { enrollmentId, programId, teiId, orgUnitId } = useLocationQuery(); const { anyStageWriteAccess } = useEnrollmentAccessContext(); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId: programId as string | undefined }); const stagesWithEventCount = useMemo(() => stages.map((stage) => { const mutatedStage = { ...stage }; @@ -79,7 +81,7 @@ const EnrollmentQuickActionsComponentPlain = ({ > } - label={i18n.t('New event')} + label={i18n.t('New {{eventLabel}}', { eventLabel })} 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 an {{eventLabel}}', { eventLabel })} onClickAction={() => onNavigationFromQuickActions(tabMode.SCHEDULE)} dataTest={'quick-action-button-schedule'} disabled={noStageAvailable} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx index 608b0e6e46..2be67019ce 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/EnrollmentPageDefault/EnrollmentQuickActions/QuickActionButton/QuickActionButton.tsx @@ -1,8 +1,9 @@ -import React, { type ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { type ComponentType } from 'react'; import { Button, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from 'capture-core/components/Tooltips/ConditionalTooltip'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; import { QuickActionButtonTypes } from './QuickActionButton.types'; const styles = { @@ -15,23 +16,26 @@ const styles = { type Props = QuickActionButtonTypes & WithStyles; -const QuickActionButtonPlain = ({ icon, label, onClickAction, dataTest, disabled = false, classes }: Props) => ( - - - ); + + ); +}; export const QuickActionButton = withStyles(styles)(QuickActionButtonPlain) as ComponentType; diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx index fa1043c8e2..9fc1bb5bf8 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/Enrollment/MissingMessage.component.tsx @@ -5,6 +5,7 @@ import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { useMissingCategoriesInProgramSelection } from '../../../hooks/useMissingCategoriesInProgramSelection'; import { scopeTypes } from '../../../metaData/helpers/constants'; +import { LabelKeys, useTermLabel } from '../../../metaData'; import { enrollmentAccessLevels } from './EnrollmentPage.constants'; import { useNavigate, buildUrlQueryString, useLocationQuery } from '../../../utils/routing'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; @@ -151,17 +152,20 @@ const styles: Readonly = { }, }; -const EnrollmentSelectionMessage = ({ enrollmentId }: { enrollmentId?: string }) => ( - - {enrollmentId ? - i18n.t('Invalid enrollment id {{enrollmentId}}.', { - enrollmentId, - interpolation: { escapeValue: false }, - }) : - i18n.t('Choose an enrollment to view the dashboard.') - } - -); +const EnrollmentSelectionMessage = ({ enrollmentId }: { enrollmentId?: string }) => { + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular]); + return ( + + {enrollmentId ? + i18n.t('Invalid {{enrollmentLabel}} id {{enrollmentId}}.', { + enrollmentLabel, + enrollmentId, + }) : + i18n.t('Choose an {{enrollmentLabel}} to view the dashboard.', { enrollmentLabel }) + } + + ); +}; type PlainProps = Record; @@ -180,6 +184,10 @@ const MissingMessagePlain = ({ const { resetTeiId } = useResetTeiId(); const { teiDisplayName, tetId } = useSelector(({ enrollmentPage }: any) => enrollmentPage); const { programId, teiId, enrollmentId } = useLocationQuery(); + const { enrollmentLabel, enrollmentsLabel, orgUnitLabel, eventLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.enrollmentPlural, LabelKeys.orgUnitSingular, LabelKeys.eventSingular], + { programId }, + ); const { trackedEntityName: tetName } = useScopeInfo(tetId); const { programName, trackedEntityName: selectedTetName } = useScopeInfo(programId); @@ -188,9 +196,10 @@ const MissingMessagePlain = ({ { missingStatus === missingStatuses.MISSING_PROGRAM_SELECTION && - {i18n.t('Choose a program to add new or see existing enrollments for {{teiDisplayName}}', { - teiDisplayName, interpolation: { escapeValue: false }, - })} + {i18n.t( + 'Choose a program to add new or see existing {{enrollmentsLabel}} for {{teiDisplayName}}', + { enrollmentsLabel, teiDisplayName }, + )} } @@ -213,13 +222,16 @@ const MissingMessagePlain = ({ missingStatus === missingStatuses.MISSING_ENROLLMENT_SELECTION_ADD_NEW &&
- {i18n.t('There are no active enrollments.')} + {i18n.t('There are no active {{enrollmentsLabel}}.', { enrollmentsLabel })}
- {i18n.t('Add new enrollment for {{teiDisplayName}} in this program.', { teiDisplayName })} + {i18n.t( + 'Add new {{enrollmentLabel}} for {{teiDisplayName}} in this program.', + { enrollmentLabel, teiDisplayName }, + )}
@@ -243,7 +255,11 @@ const MissingMessagePlain = ({ missingStatus === missingStatuses.RESTRICTED_PROGRAM_NO_ACCESS && {/* eslint-disable-next-line max-len */} - {i18n.t('You do not have permissions to access to this program, registering unit or record, contact your administrator for more information.')} + {i18n.t( + // eslint-disable-next-line max-len + 'You do not have permissions to access to this program, {{orgUnitLabel}} or record, contact your administrator for more information.', + { orgUnitLabel }, + )} } @@ -271,8 +287,8 @@ const MissingMessagePlain = ({
{/* eslint-disable-next-line max-len */} - {i18n.t('{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the {{programName}}. Choose another program that allows {{tetName}} enrollment. ', { - teiDisplayName, programName, tetName, interpolation: { escapeValue: false }, + {i18n.t('{{teiDisplayName}} is a {{tetName}} and cannot be enrolled in the {{programName}}. Choose another program that allows {{tetName}} {{enrollmentLabel}}. ', { + teiDisplayName, programName, tetName, enrollmentLabel, })}
- {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 {{enrollmentsLabel}}.', { + programName, enrollmentsLabel, })}
- {i18n.t('Create a new event in this program.')} + {i18n.t('Create a new {{eventLabel}} in this program.', { eventLabel })}
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..90c6c36a49 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,5 @@ import React from 'react'; -import i18n from '@dhis2/d2-i18n'; +import { LabelKeys, useTermLabel } from '../../../metaData'; import { ScopeSelector, useSetProgramId, @@ -31,6 +31,7 @@ export const TopBar = ({ teiDisplayName, enrollmentsAsOptions, }: TopBarProps) => { + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular], { programId }); const { setProgramIdAndResetEnrollmentContext } = useSetProgramId(); const { setOrgUnitId } = useSetOrgUnitId(); const { setEnrollmentId } = useSetEnrollmentId(); @@ -73,7 +74,7 @@ export const TopBar = ({ onSelect={id => setEnrollmentId({ enrollmentId: id })} options={enrollmentsAsOptions} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} /> ) : <>} diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts b/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts index 19fbb4ff3c..427946b700 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/epics/enrollmentPage.epics.ts @@ -2,7 +2,7 @@ import { ofType } from 'redux-observable'; import { concatMap, map, filter } from 'rxjs/operators'; import { from } from 'rxjs'; import i18n from '@dhis2/d2-i18n'; - +import { getTermLabel, getScopeInfo, LabelKeys } from '../../../../metaData'; import { enrollmentPageActionTypes, resetEnrollmentId, @@ -25,7 +25,6 @@ import { import { enrollmentAccessLevels, selectionStatus } from '../EnrollmentPage.constants'; import { buildUrlQueryString, getLocationQuery } from '../../../../utils/routing'; import { deriveTeiName } from '../../common/EnrollmentOverviewDomain/useTeiDisplayName'; -import { getScopeInfo } from '../../../../metaData'; import { scopeTypes } from '../../../../metaData/helpers/constants'; const teiQuery = id => ({ @@ -118,13 +117,17 @@ export const verifyEnrollmentIdSuccessEpic = (action$: any, store: any) => map(({ payload }) => fetchEnrollmentIdSuccess(payload)), ); -export const enrollmentIdErrorEpic = (action$: any) => +export const enrollmentIdErrorEpic = (action$: any, store: any) => action$.pipe( ofType(enrollmentPageActionTypes.FETCH_ENROLLMENT_ID_ERROR), - map(({ payload: { enrollmentId } }) => - showErrorViewOnEnrollmentPage({ - error: i18n.t('Enrollment with id "{{enrollmentId}}" does not exist', { enrollmentId }), - })), + map(({ payload: { enrollmentId } }) => { + const { programId } = store.value.enrollmentPage; + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId }); + return showErrorViewOnEnrollmentPage({ + error: i18n.t('{{enrollmentLabel}} with id "{{enrollmentId}}" does not exist', + { enrollmentLabel, enrollmentId }), + }); + }), ); // Epics for teiId diff --git a/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts b/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts index 11229927ea..e26c2cda52 100644 --- a/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts +++ b/src/core_modules/capture-core/components/Pages/Enrollment/epics/fetchEnrollment.epics.ts @@ -1,10 +1,10 @@ +import i18n from '@dhis2/d2-i18n'; import { ofType } from 'redux-observable'; import { catchError, concatMap, map } from 'rxjs/operators'; import { from, of } from 'rxjs'; import moment from 'moment'; -import i18n from '@dhis2/d2-i18n'; import { FEATURES, featureAvailable } from 'capture-core-utils'; - +import { getTermLabel, LabelKeys } from '../../../../metaData'; import { systemSettingsStore } from '../../../../metaDataMemoryStores'; import { enrollmentPageActionTypes, @@ -119,12 +119,15 @@ const handleErrorsFromNewerBackends = ({ querySingleResource, })); } - const errorMessage = - i18n.t('An error occurred while fetching enrollments. Please enter a valid url.'); + const { enrollmentsLabel } = getTermLabel([LabelKeys.enrollmentPlural], { programId }); + const errorMessage = i18n.t( + 'An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', + { enrollmentsLabel }, + ); return of(showErrorViewOnEnrollmentPage({ error: errorMessage })); }; -const handleErrorsFromOlderBackends = (error: any) => { +const handleErrorsFromOlderBackends = (error: any, programId: string) => { const { message } = error || {}; if (message) { if (message.includes(serverErrorMessages.OWNERSHIP_ACCESS_PARTIALLY_DENIED)) { @@ -137,7 +140,11 @@ const handleErrorsFromOlderBackends = (error: any) => { return fetchEnrollmentsError({ accessLevel: enrollmentAccessLevels.NO_ACCESS }); } } - const errorMessage = i18n.t('An error occurred while fetching enrollments. Please enter a valid url.'); + const { enrollmentsLabel } = getTermLabel([LabelKeys.enrollmentPlural], { programId }); + const errorMessage = i18n.t( + 'An error occurred while fetching {{enrollmentsLabel}}. Please enter a valid url.', + { enrollmentsLabel }, + ); return showErrorViewOnEnrollmentPage({ error: errorMessage }); }; @@ -165,7 +172,7 @@ export const fetchEnrollmentsEpic = (action$: any, store: any, { querySingleReso querySingleResource, }); } - return of(handleErrorsFromOlderBackends(error)); + return of(handleErrorsFromOlderBackends(error, programId)); }), map((action: any) => verifyFetchedEnrollments({ teiId, programId, action })), ); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts index 9fe72e13b7..a8c2c14d63 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPage.epics.ts @@ -100,7 +100,7 @@ export const saveNewEventSucceededEpic = (action$: EpicAction, store: Redux }), ); -export const saveNewEventFailedEpic = (action$: EpicAction) => +export const saveNewEventFailedEpic = (action$: EpicAction, store: any) => action$.pipe( ofType( addEnrollmentEventPageDefaultActionTypes.EVENT_SAVE_ERROR, @@ -110,9 +110,10 @@ export const saveNewEventFailedEpic = (action$: EpicAction) => map((action: any) => { const { serverData: { events, enrollments } } = action.meta; const rollbackEvents = events ?? enrollments[0].events; + const { programId } = store.value.enrollmentPage; return batchActions([ - saveFailed(), + saveFailed(programId), rollbackEnrollmentEvents({ events: rollbackEvents, }), diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.component.tsx index b09e5879f0..a0fa3f3408 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 { LabelKeys, TrackerProgram, useTermLabel } from '../../../../metaData'; const styles: Readonly = ({ typography }: any) => ({ container: { @@ -58,6 +58,7 @@ const EnrollmentAddEventPagePain = ({ classes, ...passOnProps }: Props & WithStyles) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId: program?.id }); if (pageFailure) { return (
@@ -93,8 +94,14 @@ 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 {{eventLabel}} yet', + { eventLabel }, + )} + indicatorEmptyText={i18n.t( + 'No indicator output for this {{eventLabel}} yet', + { eventLabel }, + )} />
diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/EnrollmentAddEventPageDefault/EnrollmentAddEventPageDefault.container.tsx index 23d7df4412..22deaa0ca3 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 { LabelKeys, TrackerProgram, useTermLabel } from '../../../../metaData'; export const EnrollmentAddEventPageDefault = ({ pageLayout, @@ -36,6 +36,7 @@ export const EnrollmentAddEventPageDefault = ({ trackedEntityInactive, }: ContainerProps) => { const { programId, stageId, orgUnitId, teiId, enrollmentId } = useLocationQuery(); + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular], { programId, stageId }); const { navigate } = useNavigate(); const dispatch = useDispatch(); @@ -146,7 +147,7 @@ export const EnrollmentAddEventPageDefault = ({ error title={i18n.t('An error has occurred')} > - {i18n.t('Program stage is invalid')} + {i18n.t('{{programStageLabel}} is invalid', { programStageLabel })} ); } @@ -159,6 +160,7 @@ export const EnrollmentAddEventPageDefault = ({ enrollmentId={enrollmentId} teiDisplayName={teiDisplayName} trackedEntityName={trackedEntityName} + stageId={stageId} stageName={selectedProgramStage?.stageForm.name} stageIcon={selectedProgramStage?.icon} eventDateLabel={selectedProgramStage?.stageForm.getLabel('occurredAt')} 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..a2181f966e 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, LabelKeys, useTermLabel } from '../../../../metaData'; import { WidgetEnrollmentEventNew } from '../../../WidgetEnrollmentEventNew'; import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { NoWriteAccessMessage } from '../../../NoWriteAccessMessage'; @@ -48,6 +48,10 @@ const NewEventWorkspacePlain = ({ const [isWarningVisible, setWarningVisible] = useState(false); const tempMode = useRef(undefined); const { stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const { programStageLabel, eventLabel } = useTermLabel( + [LabelKeys.programStageSingular, LabelKeys.eventSingular], + { programId, stageId }, + ); const onHandleSwitchTab = (newMode: string) => { if (dataEntryHasChanges) { @@ -69,7 +73,9 @@ const NewEventWorkspacePlain = ({ if (!stage) { return renderWidget( -
{i18n.t('Stage not found')}
, +
+ {i18n.t('{{programStageLabel}} not found', { programStageLabel })} +
, ); } @@ -78,7 +84,10 @@ 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..6a66183d70 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 React from 'react'; import type { Props } from './widgetStageHeader.types'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; -export const WidgetStageHeader = ({ stage }: Props) => ( -
- {stage?.stageForm.name ?? i18n.t('New Event')} -
-); +export const WidgetStageHeader = ({ stage }: Props) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular]); + return ( +
+ {stage?.stageForm.name ?? i18n.t('New {{eventLabel}}', { eventLabel })} +
+ ); +}; 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..b3149ab31f 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 { getTermLabel, LabelKeys } from '../../../../metaData'; const styles: Readonly = { container: { @@ -39,6 +40,7 @@ type ProgramStage = { type Props = { programStages: ProgramStage[]; + programId: string; onSelectProgramStage: (stageId: string) => void; onCancel: () => void; }; @@ -47,6 +49,7 @@ type ProgramStageSelectorPlainProps = Props & WithStyles; const ProgramStageSelectorComponentPlain = ({ programStages, + programId, onSelectProgramStage, onCancel, classes, @@ -57,13 +60,18 @@ const ProgramStageSelectorComponentPlain = ({ !programStage.dataAccess.write || (!programStage.repeatable && programStage.eventCount > 0) || programStage.hiddenProgramStage; + const { eventsLabel } = getTermLabel( + [LabelKeys.eventPlural], + { programId, stageId: programStage.id }, + ); return (
{ const { navigate } = useNavigate(); + const { programStageLabel, programStagesLabel, eventLabel } = useTermLabel( + [LabelKeys.programStageSingular, LabelKeys.programStagePlural, LabelKeys.eventSingular], + { programId }, + ); const { tab } = useLocationQuery(); const { error: enrollmentsError, enrollment, attributeValues } = useCommonEnrollmentDomainData( teiId, @@ -103,16 +108,20 @@ export const ProgramStageSelector = ({ programId, orgUnitId, teiId, enrollmentId <> {program ? - : i18n.t('Program Stages could not be loaded')} + : i18n.t('{{programStagesLabel}} could not be loaded', { programStagesLabel })} ); }; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx index 8aa3a1bd59..84b117c61e 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/TopBar.component.tsx @@ -1,5 +1,6 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; import { ScopeSelector, SingleLockedSelect, useReset } from '../../../ScopeSelector'; import { TopBarActions } from '../../../TopBarActions'; import type { Props } from './topBar.types'; @@ -10,6 +11,7 @@ export const EnrollmentAddEventTopBar = ({ enrollmentId, teiDisplayName, trackedEntityName, + stageId, stageName, stageIcon, eventDateLabel, @@ -24,6 +26,8 @@ export const EnrollmentAddEventTopBar = ({ enrollmentsAsOptions, }: Props) => { const { reset } = useReset(); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular], { programId, stageId }); return ( onResetEnrollmentId()} options={enrollmentsAsOptions || []} selectedValue={enrollmentId} - title={i18n.t('Enrollment')} + title={enrollmentLabel} isUserInteractionInProgress={userInteractionInProgress} /> {stageName && ( @@ -72,7 +76,7 @@ export const EnrollmentAddEventTopBar = ({ }, ]} selectedValue="alwaysPreselected" - title={i18n.t('Stage')} + title={i18n.t('{{programStageLabel}}', { programStageLabel })} isUserInteractionInProgress={userInteractionInProgress} /> diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/topBar.types.ts b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/topBar.types.ts index 2c664cff23..13a90d2857 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/topBar.types.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentAddEvent/TopBar/topBar.types.ts @@ -6,6 +6,7 @@ export type Props = { enrollmentId: string; teiDisplayName?: string; trackedEntityName?: string; + stageId?: string; stageName?: string; stageIcon?: Icon; eventDateLabel?: string; 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..6ca404d761 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.component.tsx @@ -1,7 +1,8 @@ -import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import React from 'react'; import { dataEntryIds } from 'capture-core/constants'; import type { PlainProps } from './EnrollmentEditEventPage.types'; +import { LabelKeys, useTermLabel } from '../../../metaData'; import { OrgUnitFetcher } from '../../OrgUnitFetcher'; import { TopBar } from './TopBar.container'; import { NoticeBox } from '../../NoticeBox'; @@ -62,80 +63,83 @@ export const EnrollmentEditEventPageComponent = ({ onUpdateEnrollmentEventsSuccess, onUpdateEnrollmentEventsError, userInteractionInProgress, -}: PlainProps) => ( - - - - - -); +}: PlainProps) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId: program?.id }); + return ( + + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts index 5d05232f57..1801bd7fae 100644 --- a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts +++ b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/EnrollmentEditEventPage.epics.ts @@ -16,13 +16,14 @@ export const updateEventSucceededEpic = (action$: EpicAction) => return commitEnrollmentEvent(eventId); })); -export const updateEventFailedEpic = (action$: EpicAction) => +export const updateEventFailedEpic = (action$: EpicAction, store: any) => action$.pipe( ofType( editActionTypes.EVENT_SCHEDULE_ERROR, ), map((action: any) => { const { eventId } = action.meta; - return batchActions([saveFailed(), rollbackEnrollmentEvent(eventId)]); + const { programId } = store.value.enrollmentPage; + return batchActions([saveFailed(programId), rollbackEnrollmentEvent(eventId)]); }), ); diff --git a/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx b/src/core_modules/capture-core/components/Pages/EnrollmentEditEvent/TopBar.container.tsx index d5dd0a2dc1..4a20771a43 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 { LabelKeys, useTermLabel, type ProgramStage } from '../../../metaData'; import { pageStatuses } from './EnrollmentEditEventPage.constants'; import { ScopeSelector, @@ -47,6 +47,11 @@ export const TopBar = ({ isUserInteractionInProgress, }: Props) => { const { setOrgUnitId } = useSetOrgUnitId(); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular], { + programId, + stageId: programStage?.id, + }); const { resetProgramIdAndEnrollmentContext } = useResetProgramId(); const { resetOrgUnitId } = useResetOrgUnitId(); @@ -87,7 +92,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..dccf587170 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/InvalidCategoryCombinationForOrgUnitMessage/InvalidCategoryCombinationForOrgUnitMessage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { IncompleteSelectionsMessage } from '../../../../IncompleteSelectionsMessage'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; const styles: Readonly = { incompleteMessageContainer: { @@ -11,15 +12,21 @@ const styles: Readonly = { type Props = WithStyles; -export const InvalidCategoryCombinationForOrgUnitMessagePlain = ({ classes }: Props) => ( -
- - {i18n.t('The category option is not valid for the selected organisation unit.')} - {' '} - {i18n.t('Please select a valid combination.')} - -
-); +export const InvalidCategoryCombinationForOrgUnitMessagePlain = ({ classes }: Props) => { + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); + return ( +
+ + {i18n.t( + 'The category option is not valid for the selected {{orgUnitLabel}}.', + { orgUnitLabel }, + )} + {' '} + {i18n.t('Please select a valid combination.')} + +
+ ); +}; export const InvalidCategoryCombinationForOrgUnitMessage = withStyles(styles)( InvalidCategoryCombinationForOrgUnitMessagePlain, diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx index 1284044849..8883f6a881 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/NoSelectionsInfoBox/NoSelectionsInfoBox.tsx @@ -2,6 +2,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { colors } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; const styles: Readonly = { container: { @@ -63,36 +64,42 @@ const EmptyStateIcon = () => ( const documentationLink = 'https://docs.dhis2.org/en/use/user-guides/dhis-core-version-master/tracking-individual-level-data/capture.html'; -const NoSelectionsInfoBoxPlain = ({ classes }: Props) => ( -
-
-
- -
-

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

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

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

+
+ + {i18n.t('Report data')}:{' '} + {i18n.t( + 'Choose a program and {{orgUnitLabel}} to see existing data and create new records.', + { orgUnitLabel }, + )} + + + {i18n.t('Search')}:{' '} + {i18n.t('Click \'Search\'. For program-specific results, choose a program first.')} + +
- - {i18n.t('Learn more about Capture app')} - + + {i18n.t('Learn more about Capture app')} + +
-
-); + ); +}; export const NoSelectionsInfoBox = withStyles(styles)(NoSelectionsInfoBoxPlain); diff --git a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WithoutOrgUnitSelectedMessage/WithoutOrgUnitSelectedMessage.tsx index cf5e3f0644..daa5072113 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 { LabelKeys, programTypes, TrackerProgram, useTermLabel } from '../../../../../metaData'; import { useProgramInfo } from '../../../../../hooks/useProgramInfo'; const styles: Readonly = { @@ -52,6 +52,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ }: Props) => { const { program, programType } = useProgramInfo(programId); const isTracker = programType === programTypes.TRACKER_PROGRAM; + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular], { programId }); const trackedEntityName = program instanceof TrackerProgram ? program.trackedEntityType?.name @@ -66,7 +67,7 @@ const WithoutOrgUnitSelectedMessagePlain = ({ >
- {i18n.t('Please select an organisation unit')} + {i18n.t('Please select an {{orgUnitLabel}}', { orgUnitLabel })}
{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..049e72c578 100644 --- a/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WorkingListsType/EventWorkingListsInit/Header/EventWorkingListsInitHeader.tsx +++ b/src/core_modules/capture-core/components/Pages/MainPage/MainPageBody/WorkingListsType/EventWorkingListsInit/Header/EventWorkingListsInitHeader.tsx @@ -1,8 +1,9 @@ -import { colors, spacers } from '@dhis2/ui'; import i18n from '@dhis2/d2-i18n'; +import { colors, spacers } from '@dhis2/ui'; import React, { type ComponentType } from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { type Props } from './eventWorkingListsInitHeader.types'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; export const styles = () => ({ container: { @@ -25,24 +26,27 @@ export const styles = () => ({ }); const EventWorkingListsInitHeaderPlain = - ({ children, classes: { container, headerContainer, listContainer, title } }: Props & WithStyles) => ( -
-
- ) => { + const { eventsLabel } = useTermLabel([LabelKeys.eventPlural]); + return ( +
+
- {i18n.t('Registered events')} - -
-
- {children} + + {i18n.t('Registered {{eventsLabel}}', { eventsLabel })} + +
+
+ {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..357b1d5349 100644 --- a/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx +++ b/src/core_modules/capture-core/components/Pages/New/NewPage.component.tsx @@ -14,6 +14,7 @@ import { useScopeInfo } from '../../../hooks/useScopeInfo'; import { RegistrationDataEntry } from './RegistrationDataEntry'; import { NoWriteAccessMessage } from '../../NoWriteAccessMessage'; import { IncompleteSelectionsMessage } from '../../IncompleteSelectionsMessage'; +import { LabelKeys, useTermLabel } from '../../../metaData'; const styles: Readonly = { container: { @@ -69,6 +70,7 @@ const NewPagePlain = ({ showMessageThatCategoryOptionIsInvalidForOrgUnit, ]); const orgUnitId = useSelector(({ currentSelections }: any) => currentSelections.orgUnitId); + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); return (
@@ -101,7 +103,7 @@ const NewPagePlain = ({ newPageStatus === newPageStatuses.WITHOUT_ORG_UNIT_SELECTED && <> - {i18n.t('Choose an organisation unit to start reporting')} + {i18n.t('Choose an {{orgUnitLabel}} to start reporting', { orgUnitLabel })}
} { - + {renderActionsContainer()}
)} diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts index c7c157aea8..512dac10fa 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.actions.ts @@ -22,10 +22,16 @@ export const eventNotesLoaded = () => export const updateEventNoteField = (value: string) => actionCreator(actionTypes.UPDATE_EVENT_NOTE_FIELD)({ value }); -export const requestSaveEventNote = (note: string) => - actionCreator(actionTypes.REQUEST_SAVE_EVENT_NOTE)({ note }); +export const requestSaveEventNote = (note: string, programId: string) => + actionCreator(actionTypes.REQUEST_SAVE_EVENT_NOTE)({ note, programId }); -export const startSaveEventNote = (eventUid: string, serverData: any, selections: any, clientId: string) => +export const startSaveEventNote = ( + eventUid: string, + serverData: any, + selections: any, + clientId: string, + programId: string, +) => actionCreator(actionTypes.START_SAVE_EVENT_NOTE)({ selections, clientId }, { offline: { effect: { @@ -36,6 +42,9 @@ export const startSaveEventNote = (eventUid: string, serverData: any, selections data: serverData, }, commit: { type: actionTypes.EVENT_NOTE_SAVED, meta: { selections, clientId } }, - rollback: { type: actionTypes.SAVE_EVENT_NOTE_FAILED, meta: { selections, clientId } }, + rollback: { + type: actionTypes.SAVE_EVENT_NOTE_FAILED, + meta: { selections, clientId, programId }, + }, }, }); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts index 65bc6bb88b..2357dc02ca 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Notes/viewEventNotes.epics.ts @@ -69,7 +69,7 @@ export const addNoteForViewEventEpic = (action$: any, store: any, { fromClientDa clientId, }; return batchActions([ - startSaveEventNote(eventId, serverData, state.currentSelections, clientNote.clientId), + startSaveEventNote(eventId, serverData, state.currentSelections, clientNote.clientId, payload.programId), addNote(noteKey, clientNote), ], viewEventNotesBatchActionTypes.SAVE_EVENT_NOTE_BATCH); })); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx index f8ccbe51e4..6a94c82ac8 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.component.tsx @@ -7,6 +7,7 @@ import { NewRelationship } from '../../NewRelationship/NewRelationship.container import { DiscardDialog } from '../../../Dialogs/DiscardDialog.component'; import { LinkButton } from '../../../Buttons/LinkButton.component'; import type { PlainProps } from './ViewEventNewRelationshipWrapper.types'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; const getStyles = (theme: any) => ({ container: { @@ -65,39 +66,47 @@ class ViewEventNewRelationshipWrapperPlain extends React.Component this.setState({ discardDialogOpen: false }); } - renderHeader = () => ( -
-
- {i18n.t('New event relationship')} -
-
- ); - render() { - const { classes, onCancel, ...passOnProps } = this.props; + const { classes, onCancel, programId, ...passOnProps } = this.props; + const { eventLabel, relationshipLabel } = getTermLabel( + [LabelKeys.eventSingular, LabelKeys.relationshipSingular], + { programId }, + ); return (
- {i18n.t('Adding relationship to event.')} + + {i18n.t( + 'Adding {{relationshipLabel}} to {{eventLabel}}.', + { eventLabel, relationshipLabel }, + )} + - {i18n.t('Go back to event without saving relationship')} + {i18n.t('Go back to {{eventLabel}} without saving {{relationshipLabel}}', { + eventLabel, + relationshipLabel, + })}
{ return { relationshipTypes, + programId: state.currentSelections.programId, }; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts index af8b72fc1c..7b45ac219b 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventNewRelationshipWrapper.types.ts @@ -1,3 +1,4 @@ export type PlainProps = { onCancel: () => void; + programId: string; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts index 845d480e4a..10bf62e0fb 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/Relationship/ViewEventRelationships.epics.ts @@ -1,8 +1,9 @@ +import i18n from '@dhis2/d2-i18n'; import { batchActions } from 'redux-batched-actions'; import { ofType } from 'redux-observable'; import { map, switchMap } from 'rxjs/operators'; -import i18n from '@dhis2/d2-i18n'; import uuid from 'd2-utilizr/lib/uuid'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; import { addRelationship, removeRelationship, @@ -79,11 +80,13 @@ export const addRelationshipForViewEventEpic = (action$: any, store: any) => const toEntity = payload.entity; const relationshipClientId = uuid(); + const programId = state.currentSelections.programId; + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); const clientRelationship = { clientId: relationshipClientId, from: { id: eventId, - name: i18n.t('This event'), + name: i18n.t('This {{eventLabel}}', { eventLabel }), type: 'PROGRAM_STAGE_INSTANCE', }, to: { @@ -99,9 +102,11 @@ export const addRelationshipForViewEventEpic = (action$: any, store: any) => r.to.id && r.to.id === clientRelationship.to.id) ) { + const { relationshipLabel } = getTermLabel([LabelKeys.relationshipSingular], { programId }); const message = i18n.t( - 'Relationship of type {{relationshipTypeName}} to {{entityName}} already exists', + '{{relationshipLabel}} of type {{relationshipTypeName}} to {{entityName}} already exists', { + relationshipLabel, entityName: clientRelationship.from.name, relationshipTypeName: clientRelationship.relationshipType.name, }, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.tsx index 62c7811d73..327f4e0cb3 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/AssigneeSection/AssigneeSection.container.tsx @@ -28,5 +28,6 @@ export const AssigneeSection = ({ readOnly={readOnly} onSave={onSaveAssignee} onSaveError={onSaveAssigneeError} + stageId={programStage?.id} /> ); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx index bcd0546089..d221480df0 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.component.tsx @@ -9,11 +9,10 @@ import { ViewEventSectionHeader } from '../../Section/ViewEventSectionHeader.com import { Notes } from '../../../../Notes/Notes.component'; import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; import type { PlainProps } from './NotesSection.types'; +import { getTermLabel, LabelKeys } from '../../../../../metaData'; const LoadingNotes = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Notes); -const headerText = i18n.t('Notes'); - const getStyles = (theme: any) => ({ badge: { backgroundColor: theme.palette.grey.light, @@ -36,13 +35,14 @@ type Props = PlainProps & WithStyles; class NotesSectionPlain extends React.Component { renderHeader = () => { - const { classes, notes, ready } = this.props; + const { classes, notes, ready, programId } = this.props; const count = notes ? notes.length : 0; const badgeCount = ready ? count : undefined; + const { notesLabel } = getTermLabel([LabelKeys.notePlural], { programId }); return ( @@ -50,8 +50,12 @@ class NotesSectionPlain extends React.Component { } render() { - const { classes, notes, fieldValue, onAddNote, ready, readOnly } = this.props; + const { classes, notes, fieldValue, onAddNote, ready, readOnly, programId } = this.props; const isEmpty = ready && (!notes || notes.length === 0); + const { eventLabel, notesLabel, noteLabel } = getTermLabel( + [LabelKeys.eventSingular, LabelKeys.notePlural, LabelKeys.noteSingular], + { programId }, + ); return ( { > {isEmpty && (
- {i18n.t("This event doesn't have any notes")} + {i18n.t( + "This {{eventLabel}} doesn't have any {{notesLabel}}", + { + eventLabel, + notesLabel, + }, + )}
)} {React.createElement(LoadingNotes as any, { ready, notes, readOnly, - onAddNote, + onAddNote: (note: string) => onAddNote(note, programId), onBlur: this.props.onUpdateNoteField, value: fieldValue, smallMainButton: true, + noteLabel, })}
); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx index b783a89832..17eb4bfc99 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.container.tsx @@ -8,12 +8,13 @@ const mapStateToProps = (state: any) => { notes: state.notes.viewEvent || [], ready: !notesSection.isLoading, fieldValue: notesSection.fieldValue, + programId: state.currentSelections.programId, }; }; const mapDispatchToProps = (dispatch: any) => ({ - onAddNote: (note: string) => { - dispatch(requestSaveEventNote(note)); + onAddNote: (note: string, programId: string) => { + dispatch(requestSaveEventNote(note, programId)); }, onUpdateNoteField: (value: string) => { dispatch(updateEventNoteField(value)); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts index 2a4bc4e5e4..9f9e2add8d 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/NotesSection/NotesSection.types.ts @@ -1,8 +1,9 @@ export type PlainProps = { notes?: Array; - onAddNote: (note: string) => void; + onAddNote: (note: string, programId: string) => void; onUpdateNoteField: (value: string) => void; fieldValue?: string; ready: boolean; readOnly: boolean; + programId: string; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx index dd3189f132..fc0e1afeeb 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.component.tsx @@ -11,12 +11,11 @@ import { withLoadingIndicator } from '../../../../../HOC/withLoadingIndicator'; import { ConnectedEntity } from './ConnectedEntity'; import type { Entity } from '../../../../Relationships/relationships.types'; import type { PlainProps } from './RelationshipsSection.types'; +import { getTermLabel, LabelKeys } from '../../../../../metaData'; const LoadingRelationships = withLoadingIndicator(null, props => ({ style: props.loadingIndicatorStyle }))(Relationships); -const headerText = i18n.t('Relationships'); - const getStyles = (theme: any) => ({ badge: { backgroundColor: theme.palette.grey.light, @@ -47,13 +46,14 @@ class RelationshipsSectionPlain extends React.Component { } renderHeader = () => { - const { classes, relationships, ready } = this.props; + const { classes, relationships, ready, programId } = this.props; const count = relationships ? relationships.length : 0; const badgeCount = ready ? count : undefined; + const { relationshipsLabel } = getTermLabel([LabelKeys.relationshipPlural], { programId }); return ( @@ -78,7 +78,7 @@ class RelationshipsSectionPlain extends React.Component { } render() { - const { classes, programStage, eventId, relationships, ready, readOnly } = this.props; + const { classes, programStage, programId, eventId, relationships, ready, readOnly } = this.props; const relationshipTypes = programStage.relationshipTypes || []; const hasRelationshipTypes = relationshipTypes.length > 0; @@ -86,6 +86,10 @@ class RelationshipsSectionPlain extends React.Component { programStage.relationshipTypesWhereStageIsFrom.filter(rt => rt.access.data.write); const isEmpty = ready && (!relationships || relationships.length === 0); + const { eventLabel, relationshipsLabel } = getTermLabel( + [LabelKeys.eventSingular, LabelKeys.relationshipPlural], + { programId }, + ); return hasRelationshipTypes && ( { > {isEmpty && (
- {i18n.t("This event doesn't have any relationships")} + {i18n.t( + "This {{eventLabel}} doesn't have any {{relationshipsLabel}}", + { + eventLabel, + relationshipsLabel, + }, + )}
)} {React.createElement(LoadingRelationships as any, { @@ -107,6 +117,7 @@ class RelationshipsSectionPlain extends React.Component { readOnly, smallMainButton: true, onRenderConnectedEntity: this.renderConnectedEntity, + programId, })}
); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx index 63e144aeaa..3b8baaba90 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.container.tsx @@ -7,6 +7,7 @@ const mapStateToProps = (state: any) => { const relationshipsSection = state.viewEventPage.relationshipsSection || {}; return { eventId: state.viewEventPage.eventId, + programId: state.currentSelections.programId, ready: !relationshipsSection.isLoading, relationships: state.relationships.viewEvent || [], orgUnitId: state.currentSelections.orgUnitId, diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts index 005b364885..601a1bfbb6 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/RightColumn/RelationshipsSection/RelationshipsSection.types.ts @@ -5,6 +5,7 @@ export type PlainProps = { onOpenAddRelationship: () => void; onDeleteRelationship: (clientId: string) => void; eventId: string; + programId: string; programStage: ProgramStage; ready: boolean; readOnly: boolean; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts index 418928323d..8b5d17d310 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/ViewEventComponent/ViewEvent.container.ts @@ -1,7 +1,8 @@ import { connect } from 'react-redux'; -import { batchActions } from 'redux-batched-actions'; import i18n from '@dhis2/d2-i18n'; +import { batchActions } from 'redux-batched-actions'; import { dataEntryIds, dataEntryKeys } from 'capture-core/constants'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; import { rollbackAssignee, setAssignee } from './viewEvent.actions'; import { cancelEditEventDataEntry } from '../../../WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions'; import { ViewEventComponent } from './ViewEvent.component'; @@ -29,6 +30,8 @@ const makeMapStateToProps = () => { ? getDataEntryKey(dataEntryIds.SINGLE_EVENT, dataEntryKeys.EDIT) : getDataEntryKey(dataEntryIds.SINGLE_EVENT, dataEntryKeys.VIEW); const isUserInteractionInProgress = dataEntryHasChanges(state, currentDataEntryKey); + const programId = state.currentSelections.programId; + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); return { programStage: programStageSelector(state), eventAccess: eventAccessSelector(state), @@ -39,8 +42,8 @@ const makeMapStateToProps = () => { getAssignedUserSaveContext: () => assignedUserContextSelector(state), eventId: state.viewEventPage.eventId, isEditEventPage: eventDetailsSection.showEditEvent, - feedbackEmptyText: i18n.t('No feedback for this event yet'), - indicatorEmptyText: i18n.t('No indicator output for this event yet'), + feedbackEmptyText: i18n.t('No feedback for this {{eventLabel}} yet', { eventLabel }), + indicatorEmptyText: i18n.t('No indicator output for this {{eventLabel}} yet', { eventLabel }), programRules: programRulesSelector(state), }; }; diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts index 34df6490ea..42f954f251 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/editEvent.epics.ts @@ -1,9 +1,10 @@ -import log from 'loglevel'; import i18n from '@dhis2/d2-i18n'; +import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import { ofType } from 'redux-observable'; import { switchMap } from 'rxjs/operators'; import { getErrorMessageAndDetails } from '../../../../utils/errors/getErrorMessageAndDetails'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; import { actionTypes as editEventActionTypes, eventFromUrlCouldNotBeRetrieved, @@ -22,11 +23,16 @@ export const getEventFromUrlEpic = ( const eventId = action.payload.eventId; const orgUnit = action.payload.orgUnit; const prevProgramId = store.value.currentSelections.programId; + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: prevProgramId }); return getEvent(eventId, absoluteApiPath, querySingleResource) .then((eventContainer: any) => { if (!eventContainer) { return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t( + '{{eventLabel}} could not be loaded. Are you sure it exists?', + { eventLabel }, + ), + ); } return eventFromUrlRetrieved(eventContainer, orgUnit, prevProgramId); }) @@ -35,8 +41,8 @@ export const getEventFromUrlEpic = ( log.error( errorCreator( message || - i18n.t('Event could not be loaded'))(details)); + i18n.t('{{eventLabel}} could not be loaded', { eventLabel }))(details)); return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('{{eventLabel}} could not be loaded. Are you sure it exists?', { eventLabel })); }); })); diff --git a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts index 9703cf84dd..dfd55ea3bf 100644 --- a/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts +++ b/src/core_modules/capture-core/components/Pages/ViewEvent/epics/viewEvent.epics.ts @@ -28,6 +28,7 @@ import { getCategoriesDataFromEventAsync } from './getCategoriesDataFromEvent'; import { eventWorkingListsActionTypes } from '../../../WorkingLists/EventWorkingLists'; import { resetLocationChange } from '../../../ScopeSelector/QuickSelector/actions/QuickSelector.actions'; import { buildUrlQueryString } from '../../../../utils/routing'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; export const getEventOpeningFromEventListEpic = ( action$: any, @@ -40,8 +41,12 @@ export const getEventOpeningFromEventListEpic = ( .then((eventContainer: any) => { if (!eventContainer) { return openViewEventPageFailed( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); } + const { orgUnitLabel } = getTermLabel( + [LabelKeys.orgUnitSingular], + { programId: eventContainer.event.programId }, + ); return getCoreOrgUnit({ orgUnitId: eventContainer.event.orgUnitId, onSuccess: (orgUnit: CoreOrgUnit) => startOpenEventForView(eventContainer, orgUnit), @@ -50,9 +55,10 @@ export const getEventOpeningFromEventListEpic = ( log.error( errorCreator( message || - i18n.t('Organisation unit could not be loaded'))(details)); + i18n.t('{{orgUnitLabel}} could not be loaded', { orgUnitLabel }))(details)); return openViewEventPageFailed( - i18n.t('Could not get organisation unit')); + i18n.t('Could not get {{orgUnitLabel}}', { orgUnitLabel }), + ); }, }); }) @@ -61,9 +67,9 @@ export const getEventOpeningFromEventListEpic = ( log.error( errorCreator( message || - i18n.t('Event could not be loaded'))(details)); + 'Event could not be loaded')(details)); return openViewEventPageFailed( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); }), ), ); @@ -82,7 +88,7 @@ export const getEventFromUrlEpic = ( .then((eventContainer: any) => { if (!eventContainer) { return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); } return getCategoriesDataFromEventAsync(eventContainer.event, querySingleResource) .then((categoriesData: any) => eventFromUrlRetrieved(eventContainer, prevProgramId, categoriesData)); @@ -92,9 +98,9 @@ export const getEventFromUrlEpic = ( log.error( errorCreator( message || - i18n.t('Event could not be loaded'))(details)); + 'Event could not be loaded')(details)); return eventFromUrlCouldNotBeRetrieved( - i18n.t('Event could not be loaded. Are you sure it exists?')); + i18n.t('Could not load the requested data. It may not exist or you may not have access.')); }); })); @@ -103,6 +109,10 @@ export const getOrgUnitOnUrlUpdateEpic = (action$: any) => ofType(viewEventActionTypes.EVENT_FROM_URL_RETRIEVED), map((action: any) => { const eventContainer = action.payload.eventContainer; + const { orgUnitLabel } = getTermLabel( + [LabelKeys.orgUnitSingular], + { programId: eventContainer.event.programId }, + ); return getCoreOrgUnit({ orgUnitId: eventContainer.event.orgUnitId, onSuccess: (orgUnit: CoreOrgUnit) => orgUnitRetrievedOnUrlUpdate(orgUnit, eventContainer), @@ -110,7 +120,7 @@ export const getOrgUnitOnUrlUpdateEpic = (action$: any) => const { message, details } = getErrorMessageAndDetails(error); log.error(errorCreator( message || - i18n.t('Organisation unit could not be loaded'))(details)); + i18n.t('{{orgUnitLabel}} could not be loaded', { orgUnitLabel }))(details)); return orgUnitCouldNotBeRetrievedOnUrlUpdate(eventContainer); }, }); diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts index 3b64fb4f55..b02f120787 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/EnrollmentPageLayout/LayoutComponentConfig/LayoutComponentConfig.ts @@ -295,9 +295,10 @@ export const AssigneeWidget: WidgetConfig = { export const EventNote: WidgetConfig = { Component: WidgetEventNote, - getProps: ({ dataEntryKey, dataEntryId }: any) => ({ + getProps: ({ dataEntryKey, dataEntryId, program }: any) => ({ dataEntryKey, dataEntryId, + programId: program.id, }), }; diff --git a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts index 87300d6fbf..fe2d6bebb4 100644 --- a/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts +++ b/src/core_modules/capture-core/components/Pages/common/EnrollmentOverviewDomain/enrollment.actions.ts @@ -85,7 +85,8 @@ export const updateEnrollmentEventWithoutId = (uid: string, eventData: any) => uid, }); -export const saveFailed = () => actionCreator(enrollmentSiteActionTypes.SAVE_FAILED)(); +export const saveFailed = (programId: string) => + actionCreator(enrollmentSiteActionTypes.SAVE_FAILED)({ programId }); export const updateEnrollmentAttributeValues = (attributeValues: Array<{ [key: string]: string }>) => actionCreator(enrollmentSiteActionTypes.UPDATE_ENROLLMENT_ATTRIBUTE_VALUES)({ diff --git a/src/core_modules/capture-core/components/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..41ddbd8f1d 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/DataEntry/Enrollment/DataEntryEnrollment.component.tsx @@ -6,6 +6,7 @@ import enrollmentClasses from './enrollment.module.css'; import { EnrollmentRegistrationEntry } from '../../../../../../DataEntries'; import type { Props } from './dataEntryEnrollment.types'; import { relatedStageActions } from '../../../../../../WidgetRelatedStages'; +import { getTermLabel, LabelKeys } from '../../../../../../../metaData'; const NewEnrollmentRelationshipPlain = ({ @@ -21,10 +22,14 @@ const NewEnrollmentRelationshipPlain = ExistingUniqueValueDialogActions, }: Props) => { const fieldOptions = { theme, fieldLabelMediaBasedClass: enrollmentClasses.fieldLabelMediaBased }; + const { relationshipLabel } = getTermLabel([LabelKeys.relationshipSingular], { programId }); 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 a {{relationshipLabel}}', + { relationshipLabel }, + ), }, [relatedStageActions.LINK_EXISTING_RESPONSE]: { hidden: true }, }; diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.component.tsx index d50daeab58..46aa0c020c 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.component.tsx @@ -2,8 +2,11 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ComposedRegUnitSelector } from './ComposedRegUnitSelector.component'; -import { getProgramFromProgramIdThrowIfNotFound } from '../../../../../../../metaData'; +import { getProgramFromProgramIdThrowIfNotFound, LabelKeys } from '../../../../../../../metaData'; import type { RegUnitSelectorProps } from './RegUnitSelector.types'; +import { withCustomLabels } from '../../../../../../../HOC/withCustomLabels'; + +const customLabels = [LabelKeys.orgUnitSingular] as const; const getStyles = () => ({ label: { @@ -45,11 +48,11 @@ class RegUnitSelectorPlain extends React.Component { } render() { - const { classes, onUpdateSelectedOrgUnit, programId, ...passOnProps } = this.props; + const { classes, onUpdateSelectedOrgUnit, programId, orgUnitLabel, ...passOnProps } = this.props; return ( { } } -export const RegUnitSelectorComponent = withStyles(getStyles)(RegUnitSelectorPlain); +export const RegUnitSelectorComponent = withCustomLabels(customLabels)(withStyles(getStyles)(RegUnitSelectorPlain)); diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.types.ts b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.types.ts index 228c12aaea..0d5ab9bc45 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.types.ts +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/RegisterTei/RegistrationSection/RegUnitSelector/RegUnitSelector.types.ts @@ -3,6 +3,7 @@ export type RegUnitSelectorProps = { selectedProgramId?: string | null; onUpdateSelectedOrgUnit: (orgUnit: Record | null | undefined, resetProgramSelection: boolean) => void; programId: string; + orgUnitLabel: string; }; export type ComposedRegUnitSelectorProps = { diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx index 056d2d15db..a932c7ef36 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx @@ -33,6 +33,7 @@ type Props = { onSetOrgUnit: (searchId: string, orgUnit?: Record) => void; onFilterOrgUnits: (searchId: string, searchText: string) => void; searchAttempted?: boolean; + orgUnitLabel: string; }; const orgUnitFieldStyles = { @@ -79,7 +80,7 @@ export class SearchOrgUnitSelector extends React.Component { getErrorMessage = () => { if (!this.isValid() && this.props.searchAttempted) { - return i18n.t('Please select an organisation unit.'); + return i18n.t('Please select an {{orgUnitLabel}}.', { orgUnitLabel: this.props.orgUnitLabel }); } return null; } @@ -112,11 +113,11 @@ export class SearchOrgUnitSelector extends React.Component { } renderOrgUnitScopeSelector = () => { - const { selectedOrgUnitScope } = this.props; + const { selectedOrgUnitScope, orgUnitLabel } = this.props; return ( { } renderOrgUnitField = () => { - const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText } = this.props; + const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText, orgUnitLabel } = this.props; return ( { const searchId = props.searchId; + const teiSearch = (state as any).teiSearch[searchId]; + const programId: string | undefined = teiSearch.selectedProgramId; const filteredRoots = getOrgUnitRoots(searchId); const roots = filteredRoots || getOrgUnitRoots('searchRoots'); return { - selectedOrgUnit: (state as any).teiSearch[searchId].selectedOrgUnit, - selectedOrgUnitScope: (state as any).teiSearch[searchId].selectedOrgUnitScope, + selectedOrgUnit: teiSearch.selectedOrgUnit, + selectedOrgUnitScope: teiSearch.selectedOrgUnitScope, treeRoots: roots, - treeSearchText: (state as any).teiSearch[searchId].orgUnitsSearchText, - treeReady: !(state as any).teiSearch[searchId].orgUnitsLoading, - treeKey: (state as any).teiSearch[searchId].orgUnitsSearchText || 'initial', + treeSearchText: teiSearch.orgUnitsSearchText, + treeReady: !teiSearch.orgUnitsLoading, + treeKey: teiSearch.orgUnitsSearchText || 'initial', + orgUnitLabel: getTermLabel([LabelKeys.orgUnitSingular], { programId }).orgUnitLabel, }; }; 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..87ca638d0f 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 @@ -8,6 +8,7 @@ import { SearchProgramSelector } from './SearchProgramSelector/SearchProgramSele import { Section, SectionHeaderSimple } from '../../../../Section'; import { ResultsPageSizeContext } from '../../../shared-contexts'; import type { Props } from './TeiSearch.types'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; const getStyles = (theme: any) => ({ container: { @@ -26,6 +27,7 @@ const getStyles = (theme: any) => ({ const TeiSearchPlain = (props: Props & WithStyles) => { const [programSectionOpen, setProgramSectionOpen] = useState(true); + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural], { programId: props.selectedProgramId }); const getFormId = useCallback((searchGroupId) => { const contextId = props.selectedProgramId || props.selectedTrackedEntityTypeId || ''; @@ -99,7 +101,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 {{attributesLabel}}', { attributesLabel }); const collapsed = props.openSearchGroupSection !== searchGroupId; const unsupportedAttributes = sg.unsupportedAttributes; return ( diff --git a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx index 9aaf7e90fe..98a5eee698 100644 --- a/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx +++ b/src/core_modules/capture-core/components/Pages/common/TEIRelationshipsWidget/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx @@ -15,6 +15,7 @@ import { import { D2Form } from '../../../../../D2Form'; import { SearchOrgUnitSelector } from '../SearchOrgUnitSelector/SearchOrgUnitSelector.container'; import { withGotoInterface } from '../../../../../FormFields/New'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; import type { SearchGroup } from '../../../../../../metaData'; const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); @@ -45,6 +46,20 @@ type State = { showMissingSearchCriteriaModal: boolean; }; +const MinAttributesRequiredMessage = ({ count }: { count: number }) => { + const { attributeLabel } = useTermLabel([LabelKeys.attributeSingular]); + return ( + <> + {i18n.t('Fill in at least {{count}} {{attributeLabel}} to search', { + count, + attributeLabel, + defaultValue: 'Fill in at least {{count}} {{attributeLabel}} to search', + defaultValue_plural: 'Fill in at least {{count}} attributes to search', + })} + + ); +}; + type OwnProps = { id: string; searchGroupId: string; @@ -55,6 +70,7 @@ type OwnProps = { searchGroup: SearchGroup; attributesWithValuesCount: number; formsValues: { [formElement: string]: any }; + attributesLabel: string; }; type Props = OwnProps & WithStyles; @@ -163,13 +179,7 @@ class SearchFormPlain extends React.Component { return (
- { - i18n.t('Fill in at least {{count}} attribute to search', { - count: searchGroup.minAttributesRequiredToSearch, - defaultValue: 'Fill in at least {{count}} attribute to search', - defaultValue_plural: 'Fill in at least {{count}} attributes to search', - }) - } +
); } @@ -217,7 +227,7 @@ class SearchFormPlain extends React.Component { } const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) : - i18n.t('Search by attributes'); + i18n.t('Search by {{attributesLabel}}', { attributesLabel: this.props.attributesLabel }); return (
{ } } -export const TeiSearchFormComponent = withStyles(getStyles)(SearchFormPlain) as any; +const TeiSearchFormWithStyles = withStyles(getStyles)(SearchFormPlain) as any; + +export const TeiSearchFormComponent = (props: Omit) => { + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural]); + 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..29d3350a6f 100644 --- a/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.tsx +++ b/src/core_modules/capture-core/components/Pages/common/WidgetEventEditWrapper/WidgetEventEditWrapper.tsx @@ -5,6 +5,7 @@ import { IncompleteSelectionsMessage } from '../../../IncompleteSelectionsMessag import { WidgetEventEdit } from '../../../WidgetEventEdit'; import type { Props } from '../../../WidgetEventEdit/widgetEventEdit.types'; import { useMetadataForProgramStage } from '../../../DataEntries/common/ProgramStage/useMetadataForProgramStage'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; type WidgetProps = { pageStatus: string; @@ -22,18 +23,27 @@ export const WidgetEventEditWrapper = ({ pageStatus, ...passOnProps }: WidgetPro isLoading, isError, } = useMetadataForProgramStage({ programId, stageId }); + const { enrollmentLabel, orgUnitLabel, eventLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.orgUnitSingular, LabelKeys.eventSingular], + { programId }, + ); if (pageStatus === pageStatuses.WITHOUT_ORG_UNIT_SELECTED) { return ( - {i18n.t('Choose an organisation unit to start reporting')} + {i18n.t('Choose an {{orgUnitLabel}} to start reporting', { orgUnitLabel })} ); } if (pageStatus === pageStatuses.MISSING_DATA) { return ( - {i18n.t('The enrollment event data could not be found')} + + {i18n.t('The {{enrollmentLabel}} {{eventLabel}} data could not be found', { + enrollmentLabel, + eventLabel, + })} + ); } diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx index ffffb97b53..cc6fc68591 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.tsx @@ -4,6 +4,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { ConditionalTooltip } from '../Tooltips/ConditionalTooltip'; import type { Props, Access, ReadOnlyMessageInput } from './ReadOnlyBadge.types'; +import { LabelKeys, useTermLabel } from '../../metaData'; const styles = { label: { @@ -11,7 +12,8 @@ const styles = { }, } as const; -const getEnrollmentMessage = (): string => i18n.t('You only have view access to this enrollment'); +const getEnrollmentMessage = (enrollmentLabel: string): string => + i18n.t('You only have view access to this {{enrollmentLabel}}', { enrollmentLabel }); const getProgramMessage = (): string => i18n.t('You only have view access to this program'); @@ -19,13 +21,19 @@ const getTrackedEntityMessage = (trackedEntityName: string | undefined): string ? i18n.t('You only have view access to this {{trackedEntityName}}', { trackedEntityName, escapeValue: false }) : i18n.t('You only have view access to this tracked entity type')); -const getProgramStageMessage = (multipleStages: boolean): string => (multipleStages - ? i18n.t('You only have view access to these program stages') - : i18n.t('You only have view access to this program stage')); +const getProgramStageMessage = ( + multipleStages: boolean, + programStageLabel: string, + programStagesLabel: string, +): string => (multipleStages + ? i18n.t('You only have view access to these {{programStagesLabel}}', { programStagesLabel }) + : i18n.t('You only have view access to this {{programStageLabel}}', { programStageLabel })); -const getExpiredMessage = (): string => i18n.t('This event is outside the editing period'); +const getExpiredMessage = (eventLabel: string): string => + i18n.t('This {{eventLabel}} is outside the editing period', { eventLabel }); -const getCompletedEventMessage = (): string => i18n.t('This event has been completed'); +const getCompletedEventMessage = (eventLabel: string): string => + i18n.t('This {{eventLabel}} has been completed', { eventLabel }); const getDeactivatedMessage = (trackedEntityName: string | undefined): string => (trackedEntityName ? i18n.t('This {{trackedEntityName}} is deactivated', { trackedEntityName, escapeValue: false }) @@ -40,15 +48,19 @@ const getReadOnlyMessage = ({ canEditCompletedEvent, withinCompleteEventsExpiry, trackedEntityInactive, + enrollmentLabel, + programStageLabel, + programStagesLabel, + eventLabel, }: ReadOnlyMessageInput): string => { if (trackedEntityInactive) return getDeactivatedMessage(trackedEntityName); - if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(); + if (!access.program && !access.trackedEntityType && !access.programStage) return getEnrollmentMessage(enrollmentLabel); if (!access.program) return getProgramMessage(); if (!access.trackedEntityType) return getTrackedEntityMessage(trackedEntityName); - if (!access.programStage) return getProgramStageMessage(multipleStages); - if (!eventWithinValidPeriod) return getExpiredMessage(); - if (!canEditCompletedEvent) return getCompletedEventMessage(); - if (!withinCompleteEventsExpiry) return getExpiredMessage(); + if (!access.programStage) return getProgramStageMessage(multipleStages, programStageLabel, programStagesLabel); + if (!eventWithinValidPeriod) return getExpiredMessage(eventLabel); + if (!canEditCompletedEvent) return getCompletedEventMessage(eventLabel); + if (!withinCompleteEventsExpiry) return getExpiredMessage(eventLabel); return ''; }; @@ -63,8 +75,18 @@ const ReadOnlyBadgePlain = ({ trackedEntityName, trackedEntityInactive = false, inlineLabel = false, + programId, + stageId, classes, }: Props & WithStyles) => { + const { enrollmentLabel, programStagesLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.programStagePlural], + { programId }, + ); + const { programStageLabel, eventLabel } = useTermLabel( + [LabelKeys.programStageSingular, LabelKeys.eventSingular], + { programId, stageId }, + ); const access: Access = { program: programWriteAccess, trackedEntityType: trackedEntityTypeWriteAccess, @@ -78,6 +100,10 @@ const ReadOnlyBadgePlain = ({ canEditCompletedEvent, withinCompleteEventsExpiry, trackedEntityInactive, + enrollmentLabel, + programStageLabel, + programStagesLabel, + eventLabel, }); if (!message) return null; diff --git a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts index a7ada9cb08..dc83d82818 100644 --- a/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts +++ b/src/core_modules/capture-core/components/ReadOnlyBadge/ReadOnlyBadge.types.ts @@ -9,6 +9,8 @@ export type Props = { trackedEntityName?: string; trackedEntityInactive?: boolean; inlineLabel?: boolean; + programId?: string; + stageId?: string; }; export type Access = { @@ -25,4 +27,8 @@ export type ReadOnlyMessageInput = { canEditCompletedEvent: boolean; withinCompleteEventsExpiry: boolean; trackedEntityInactive: boolean; + enrollmentLabel: string; + programStageLabel: string; + programStagesLabel: string; + eventLabel: string; }; diff --git a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx index 0762e0d941..e042b677ea 100644 --- a/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx +++ b/src/core_modules/capture-core/components/Relationships/Relationships.component.tsx @@ -1,11 +1,12 @@ +import i18n from '@dhis2/d2-i18n'; import * as React from 'react'; import { cx } from '@emotion/css'; -import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { IconButton } from 'capture-ui'; import { IconDelete16, Button, colors } from '@dhis2/ui'; import { DirectionalArrow } from '../../utils/rtl'; import type { RelationshipType } from '../../metaData'; +import { getTermLabel, LabelKeys } from '../../metaData'; import type { Relationship, Entity } from './relationships.types'; const styles: Readonly = (theme: any) => ({ @@ -63,8 +64,11 @@ const styles: Readonly = (theme: any) => ({ }, }); -const fromNames = { - PROGRAM_STAGE_INSTANCE: i18n.t('This event'), +const getFromNames = (programId: string) => { + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); + return { + PROGRAM_STAGE_INSTANCE: i18n.t('This {{eventLabel}}', { eventLabel }), + }; }; type PlainProps = { @@ -78,6 +82,7 @@ type PlainProps = { currentEntityId: string; smallMainButton: boolean; relationshipsRef: (instance: any) => void; + programId: string; }; type Props = PlainProps & WithStyles; @@ -105,7 +110,7 @@ class RelationshipsPlain extends React.Component { const { onRenderConnectedEntity } = this.props; if (entity.id === this.props.currentEntityId) { - return fromNames[entity.type]; + return getFromNames(this.props.programId)[entity.type]; } return onRenderConnectedEntity ? onRenderConnectedEntity(entity) : entity.name; @@ -169,8 +174,10 @@ class RelationshipsPlain extends React.Component { writableRelationshipTypes, relationshipsRef, smallMainButton, + programId, } = this.props; const canCreate = !readOnly && writableRelationshipTypes.length > 0; + const { relationshipLabel } = getTermLabel([LabelKeys.relationshipSingular], { programId }); return (
@@ -185,7 +192,7 @@ class RelationshipsPlain extends React.Component { dataTest="add-relationship-button" secondary > - {i18n.t('Add relationship')} + {i18n.t('Add {{relationshipLabel}}', { relationshipLabel })}
diff --git a/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/OrgUnitSelector.component.tsx b/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/OrgUnitSelector.component.tsx index a3e97539c9..4388cb9fa4 100644 --- a/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/OrgUnitSelector.component.tsx +++ b/src/core_modules/capture-core/components/ScopeSelector/QuickSelector/OrgUnitSelector.component.tsx @@ -5,6 +5,10 @@ import { SelectorBarItem, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { OrgUnitField } from '../../FormFields/New'; import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; +import { LabelKeys } from '../../../metaData'; + +const customLabels = [LabelKeys.orgUnitSingular] as const; const styles = () => ({ selectBarMenu: { @@ -28,6 +32,7 @@ type OwnProps = { previousOrgUnitId?: string; isReadOnly?: boolean; tooltip?: boolean; + orgUnitLabel: string; }; type Props = OwnProps & WithStyles; @@ -54,16 +59,20 @@ class OrgUnitSelectorPlain extends Component { } render() { - const { selectedOrgUnitId, selectedOrgUnit, previousOrgUnitId, onReset, isReadOnly, tooltip, classes } = this.props; + const { + selectedOrgUnitId, selectedOrgUnit, previousOrgUnitId, onReset, isReadOnly, tooltip, classes, orgUnitLabel, + } = this.props; return ( this.setState({ open })} @@ -89,4 +98,4 @@ class OrgUnitSelectorPlain extends Component { } } -export const OrgUnitSelector = withStyles(styles)(OrgUnitSelectorPlain); +export const OrgUnitSelector = withCustomLabels(customLabels)(withStyles(styles)(OrgUnitSelectorPlain)); 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..807d0bdd9e 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 @@ -4,6 +4,7 @@ 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 { LabelKeys, useTermLabel } from '../../../../metaData'; import { OptionLabel } from '../../OptionLabel'; const styles = () => ({ @@ -31,6 +32,7 @@ type OwnProps = { type Props = OwnProps & WithStyles; const ProgramListPlain = ({ programOptions, programsArray, onChange, onResetOrgUnit, classes }: Props) => { + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); const areAllProgramsAvailable = programOptions.length === programsArray.filter(program => program.access.data.read).length; @@ -59,7 +61,10 @@ 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 {{orgUnitLabel}}', + { orgUnitLabel }, + )} 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..072ef4b522 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, useTermLabel, LabelKeys } from '../../metaData'; const getStyles: Readonly = { half: { @@ -91,6 +91,7 @@ function renderFooterContent(args: { searchGroupsForSelectedScope: SearchGroups; availableSearchOption?: AvailableSearchOption; trackedEntityName: string; + attributesLabel: string; }) { if (args.isLoading) { return ; @@ -114,8 +115,9 @@ function renderFooterContent(args: { footerNodes.push( @@ -144,6 +146,7 @@ const Index = ({ getInitialSearchScopeType(preselectedProgramId), ); const { trackedEntityName } = useScopeInfo(selectedSearchScopeId ?? null); + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural], { programId: selectedSearchScopeId }); const titleText = useScopeTitleText(selectedSearchScopeId ?? null); const { searchOption: availableSearchOption, @@ -212,6 +215,7 @@ const Index = ({ searchGroupsForSelectedScope, availableSearchOption, trackedEntityName, + attributesLabel, })} ); diff --git a/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.tsx b/src/core_modules/capture-core/components/SearchBox/SearchForm/SearchForm.component.tsx index 5fc334721a..23f753ad36 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 { useTermLabel, LabelKeys } from '../../../metaData'; const styles: Readonly = (theme: any) => ({ searchDomainsContainer: { @@ -83,6 +84,30 @@ const expandTheFirstForm = (searchGroupsForSelectedScope, expandedFormId, setExp }); }; +type FormInformativeMessageProps = { + minAttributesRequiredToSearch: number; + className: string; + programId?: string; +}; + +const FormInformativeMessage = ({ + minAttributesRequiredToSearch, + className, + programId, +}: FormInformativeMessageProps) => { + const { attributeLabel } = useTermLabel([LabelKeys.attributeSingular], { programId }); + return ( +
+ {i18n.t('Fill in at least {{count}} {{attributeLabel}} to search', { + count: minAttributesRequiredToSearch, + attributeLabel, + defaultValue: 'Fill in at least {{count}} {{attributeLabel}} to search', + defaultValue_plural: 'Fill in at least {{count}} attributes to search', + })} +
+ ); +}; + const SearchFormIndex = ({ searchViaUniqueIdOnScopeTrackedEntityType, searchViaUniqueIdOnScopeProgram, @@ -112,6 +137,7 @@ const SearchFormIndex = ({ const [error, setError] = useState(false); const [expandedFormId, setExpandedFormId] = useState(null); + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural], { programId: selectedSearchScopeId }); useEffect(() => { setExpandedFormId(null); @@ -182,17 +208,6 @@ const SearchFormIndex = ({ } }; - const FormInformativeMessage = ({ minAttributesRequiredToSearch }) => - (
- { - i18n.t('Fill in at least {{count}} attribute to search', { - count: minAttributesRequiredToSearch, - defaultValue: 'Fill in at least {{count}} attribute to search', - defaultValue_plural: 'Fill in at least {{count}} attributes to search', - }) - } -
); - const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && expandedFormId && selectedSearchScopeId) { // Don't trigger search if the event is coming from a SimpleSingleSelect component @@ -288,7 +303,7 @@ const SearchFormIndex = ({ minAttributesRequiredToSearch, unsupportedAttributes, }) => { - const searchByText = i18n.t('Search by attributes'); + const searchByText = i18n.t('Search by {{attributesLabel}}', { attributesLabel }); const isSearchSectionCollapsed = !(expandedFormId === formId); return (
@@ -337,6 +352,8 @@ const SearchFormIndex = ({
{!!unsupportedAttributes?.length && ( @@ -375,6 +392,7 @@ const SearchFormIndex = ({ resultsPageSize, error, expandedFormId, + attributesLabel, ]); }; diff --git a/src/core_modules/capture-core/components/SearchBox/SearchResults/SearchResults.component.tsx b/src/core_modules/capture-core/components/SearchBox/SearchResults/SearchResults.component.tsx index 0ba4299708..d648bf3534 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, useTermLabel, LabelKeys } from '../../../metaData'; const SearchPagination = withNavigation()(Pagination); @@ -124,6 +124,7 @@ const SearchResultsIndex = ({ const currentProgramId = (currentSearchScopeType === searchScopes.PROGRAM) ? currentSearchScopeId : ''; const { trackedEntityName } = useScopeInfo(currentSearchScopeId); + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural], { programId: currentProgramId }); return (<>
); @@ -114,6 +122,7 @@ export const SearchStatusPlain = ({ diff --git a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx index 7eb9c2440d..52e6670065 100644 --- a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx +++ b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.component.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { SelectionBoxes, withDefaultFieldContainer, @@ -65,11 +66,11 @@ export class SearchOrgUnitSelector extends React.Component { - const { selectedOrgUnitScope } = this.props; + const { selectedOrgUnitScope, orgUnitLabel } = this.props; return ( { if (!this.isValid() && this.props.searchAttempted) { - return i18n.t('Please select an organisation unit.'); + return i18n.t('Please select an {{orgUnitLabel}}.', { orgUnitLabel: this.props.orgUnitLabel }); } return null; } @@ -111,10 +112,10 @@ export class SearchOrgUnitSelector extends React.Component { - const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText } = this.props; + const { selectedOrgUnit, treeRoots, treeReady, treeKey, treeSearchText, orgUnitLabel } = this.props; return ( { const searchId = props.searchId; + const teiSearch = state.teiSearch[searchId]; + const programId = teiSearch.selectedProgramId; const filteredRoots = getOrgUnitRoots(searchId); const roots = filteredRoots || getOrgUnitRoots('searchRoots'); return { - selectedOrgUnit: state.teiSearch[searchId].selectedOrgUnit, - selectedOrgUnitScope: state.teiSearch[searchId].selectedOrgUnitScope, + selectedOrgUnit: teiSearch.selectedOrgUnit, + selectedOrgUnitScope: teiSearch.selectedOrgUnitScope, treeRoots: roots, - treeSearchText: state.teiSearch[searchId].orgUnitsSearchText, - treeReady: !state.teiSearch[searchId].orgUnitsLoading, - treeKey: state.teiSearch[searchId].orgUnitsSearchText || 'initial', + treeSearchText: teiSearch.orgUnitsSearchText, + treeReady: !teiSearch.orgUnitsLoading, + treeKey: teiSearch.orgUnitsSearchText || 'initial', + orgUnitLabel: getTermLabel([LabelKeys.orgUnitSingular], { programId }).orgUnitLabel, }; }; diff --git a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts index 8a5222b7f2..9c3a4b18c1 100644 --- a/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts +++ b/src/core_modules/capture-core/components/TeiSearch/SearchOrgUnitSelector/SearchOrgUnitSelector.types.ts @@ -10,4 +10,5 @@ export type SearchOrgUnitSelectorProps = { onSetOrgUnit: (searchId: string, orgUnit?: any) => void; onFilterOrgUnits: (searchId: string, searchText?: string) => void; searchAttempted?: boolean; + orgUnitLabel: string; }; diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx index 5db90c8754..0bf745378d 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.component.tsx @@ -1,7 +1,7 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import { SearchGroup } from '../../metaData'; +import { SearchGroup, useTermLabel, LabelKeys } from '../../metaData'; import { UnsupportedAttributesNotification } from '../../utils/warnings'; import { TeiSearchForm } from './TeiSearchForm/TeiSearchForm.container'; import { TeiSearchResults } from './TeiSearchResults/TeiSearchResults.container'; @@ -105,7 +105,7 @@ class TeiSearchPlain extends React.Component, const isUnique = sg.unique; const header = isUnique ? i18n.t('Search {{uniqueAttrName}}', { uniqueAttrName: sg.searchForm.getElements()[0].formName }) : - i18n.t('Search by attributes'); + i18n.t('Search by {{attributesLabel}}', { attributesLabel: this.props.attributesLabel }); const collapsed = this.props.openSearchGroupSection !== searchGroupId; const unsupportedAttributes = sg.unsupportedAttributes; return ( @@ -175,4 +175,9 @@ class TeiSearchPlain extends React.Component, } } -export const TeiSearchComponent = withStyles(styles)(TeiSearchPlain); +const TeiSearchWithStyles = withStyles(styles)(TeiSearchPlain); + +export const TeiSearchComponent = (props: Omit) => { + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural], { programId: props.selectedProgramId }); + return ; +}; diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts index 82aeae9670..76cffa0e48 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearch.types.ts @@ -23,4 +23,8 @@ export type OwnProps = { resultsPageSize: number; }; -export type Props = OwnProps & DispatchersFromRedux & PropsFromRedux; +type InjectedLabels = { + attributesLabel: string; +}; + +export type Props = OwnProps & DispatchersFromRedux & PropsFromRedux & InjectedLabels; diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx index b91014722b..2ea20c0715 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.component.tsx @@ -16,6 +16,7 @@ import { D2Form } from '../../D2Form'; import { SearchOrgUnitSelector } from '../SearchOrgUnitSelector/SearchOrgUnitSelector.container'; import type { Props } from './TeiSearchForm.types'; import { withGotoInterface } from '../../FormFields/New'; +import { useTermLabel, LabelKeys } from '../../../metaData'; const TeiSearchOrgUnitSelector = withGotoInterface()(SearchOrgUnitSelector); @@ -45,6 +46,20 @@ type State = { showMissingSearchCriteriaModal: boolean; }; +const MinAttributesRequiredMessage = ({ count }: { count: number }) => { + const { attributeLabel } = useTermLabel([LabelKeys.attributeSingular]); + return ( + <> + {i18n.t('Fill in at least {{count}} {{attributeLabel}} to search', { + count, + attributeLabel, + defaultValue: 'Fill in at least {{count}} {{attributeLabel}} to search', + defaultValue_plural: 'Fill in at least {{count}} attributes to search', + })} + + ); +}; + class SearchFormPlain extends React.Component, State> { formInstance: any; orgUnitSelectorInstance: typeof SearchOrgUnitSelector | undefined; @@ -143,13 +158,7 @@ class SearchFormPlain extends React.Component, return (
- { - i18n.t('Fill in at least {{count}} attribute to search', { - count: searchGroup.minAttributesRequiredToSearch, - defaultValue: 'Fill in at least {{count}} attribute to search', - defaultValue_plural: 'Fill in at least {{count}} attributes to search', - }) - } +
); } @@ -197,7 +206,7 @@ class SearchFormPlain extends React.Component, } const searchButtonText = searchGroup.unique ? this.getUniqueSearchButtonText(searchForm) - : i18n.t('Search by attributes'); + : i18n.t('Search by {{attributesLabel}}', { attributesLabel: this.props.attributesLabel }); return (
, } } -export const TeiSearchFormComponent = withStyles(styles)(SearchFormPlain); +const TeiSearchFormWithStyles = withStyles(styles)(SearchFormPlain); + +export const TeiSearchFormComponent = (props: Omit) => { + const { attributesLabel } = useTermLabel([LabelKeys.attributePlural]); + return ; +}; diff --git a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts index abf5f903ab..e14653c659 100644 --- a/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts +++ b/src/core_modules/capture-core/components/TeiSearch/TeiSearchForm/TeiSearchForm.types.ts @@ -10,4 +10,5 @@ export type Props = { searchGroup: SearchGroup; attributesWithValuesCount: number; formsValues: { [formElement: string]: any }; + attributesLabel: string; }; diff --git a/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx b/src/core_modules/capture-core/components/TopBarActions/TopBarActions.component.tsx index 2a8c8fce67..fe86bca575 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 { LabelKeys, scopeTypes, useTermLabel } from '../../metaData'; import { useScopeInfo } from '../../hooks/useScopeInfo'; import type { PlainProps } from './TopBarActions.types'; @@ -26,6 +26,7 @@ const ActionButtonsPlain = ({ openConfirmDialog, }: PlainProps & WithStyles) => { const { trackedEntityName, scopeType, programName } = useScopeInfo(selectedProgramId); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId: selectedProgramId }); const [openSearch, setOpenSearch] = useState(false); useEffect(() => { @@ -65,7 +66,7 @@ const ActionButtonsPlain = ({ trackedEntityType: trackedEntityName, interpolation: { escapeValue: false }, }) - : i18n.t('Create new event') + : i18n.t('Create new {{eventLabel}}', { eventLabel }) } )} diff --git a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx index b9309f7d42..351f0f0458 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx +++ b/src/core_modules/capture-core/components/WidgetAssignee/DisplayMode.component.tsx @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n'; import { Button, colors, spacers, spacersNum, UserAvatar } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { Assignee } from './WidgetAssignee.types'; +import { LabelKeys, useTermLabel } from '../../metaData'; const styles = () => ({ wrapper: { @@ -32,32 +33,38 @@ type Props = { onEdit: () => void; readOnly?: boolean; avatarId?: string; + programId?: string; + stageId?: string; } & WithStyles; -const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, classes }: Props) => ( - assignee ? ( -
-
- {i18n.t('Assigned to')} - - {assignee.name} +const DisplayModePlain = ({ assignee, onEdit, readOnly = false, avatarId, programId, stageId, classes }: Props) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId, stageId }); + if (assignee) { + return ( +
+
+ {i18n.t('Assigned to')} + + {assignee.name} +
+ {!readOnly && ( + + )}
- {!readOnly && ( - - )} -
- ) : ( + ); + } + return (
- {i18n.t('No one is assigned to this event')} + {i18n.t('No one is assigned to this {{eventLabel}}', { eventLabel })}
{!readOnly && (
- ) -); + ); +}; export const DisplayMode = withStyles(styles)(DisplayModePlain); diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.tsx b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.tsx index 1522ff65ba..38ebf75c96 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.tsx +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.component.tsx @@ -19,7 +19,7 @@ const styles = () => ({ type Props = PlainProps & WithStyles ; -const WidgetAssigneePlain = ({ assignee, readOnly, onSet, avatarId, classes }: Props) => { +const WidgetAssigneePlain = ({ assignee, readOnly, onSet, avatarId, programId, stageId, classes }: Props) => { const [open, setOpen] = useState(true); const [editMode, setEditMode] = useState(false); @@ -52,6 +52,8 @@ const WidgetAssigneePlain = ({ assignee, readOnly, onSet, avatarId, classes }: P onEdit={() => setEditMode(true)} readOnly={readOnly} avatarId={avatarId} + programId={programId} + stageId={stageId} /> )}
diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.tsx b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.tsx index d9e8fc47d0..90705bea88 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.tsx +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.container.tsx @@ -4,7 +4,7 @@ import { WidgetAssigneeComponent } from './WidgetAssignee.component'; import { useUserAvatar, useAssigneeMutation } from './hooks'; const WidgetAssigneeWithHooks = (props: Props) => { - const { assignee, readOnly, getSaveContext, onSave, onSaveError } = props; + const { assignee, readOnly, getSaveContext, onSave, onSaveError, programId, stageId } = props; const { avatarId, isLoading } = useUserAvatar(assignee?.id); const onSet = useAssigneeMutation({ assignee, getSaveContext, onSave, onSaveError }); @@ -18,6 +18,8 @@ const WidgetAssigneeWithHooks = (props: Props) => { readOnly={readOnly} avatarId={avatarId} onSet={onSet} + programId={programId} + stageId={stageId} /> ); }; diff --git a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.ts b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.ts index e896813f36..75ca1e8613 100644 --- a/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.ts +++ b/src/core_modules/capture-core/components/WidgetAssignee/WidgetAssignee.types.ts @@ -13,6 +13,8 @@ export type Props = { getSaveContext: () => { event: Record }; onSave: (newAssignee: Assignee) => void; onSaveError: (prevAssignee: Assignee | null) => void; + programId?: string; + stageId?: string; }; export type PlainProps = { @@ -20,4 +22,6 @@ export type PlainProps = { readOnly: boolean; onSet: (user: Assignee | null) => void; avatarId?: string; + programId?: string; + stageId?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx index 79c8583d90..3c40fec2a4 100644 --- a/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx +++ b/src/core_modules/capture-core/components/WidgetBreakingTheGlass/WidgetBreakingTheGlass.component.tsx @@ -10,6 +10,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { PlainProps } from './WidgetBreakingTheGlass.types'; import { Widget } from '../Widget'; +import { LabelKeys, useTermLabel } from '../../metaData'; const styles: Readonly = ({ typography }: any) => ({ title: { @@ -23,10 +24,6 @@ const styles: Readonly = ({ typography }: any) => ({ }, }); -const noticeBoxTitle = i18n.t('This program is protected'); -const reasonHeader = i18n.t('Reason to check for enrollments'); -const reasonPlaceholder = i18n.t('Describe the reason you are checking for enrollments in this protected program'); - type Props = PlainProps & WithStyles; const WidgetBreakingTheGlassPlain = ({ @@ -39,6 +36,7 @@ const WidgetBreakingTheGlassPlain = ({ setReason(value); }, [setReason]); const disabled = useMemo(() => reason.length === 0, [reason]); + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural]); return (
@@ -49,18 +47,26 @@ const WidgetBreakingTheGlassPlain = ({ >
- {i18n.t('Check for enrollments')} + {i18n.t('Check for {{enrollmentsLabel}}', { enrollmentsLabel })}

- - {i18n.t('You must provide a reason to check for enrollments in this protected program.')} + + {i18n.t( + 'You must provide a reason to check for {{enrollmentsLabel}} in this protected program.', + { enrollmentsLabel }, + )} {' '} {i18n.t('All activity will be logged.')}
- - - - - - -); + + + + + + + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.container.tsx index b1353e9d3f..7b1faf4a8a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Complete/CompleteModal/CompleteModal.container.tsx @@ -5,7 +5,14 @@ import { CompleteModalComponent } from './CompleteModal.component'; import { eventStatuses, plainStatus } from '../../../constants/status.const'; import type { Props } from './completeModal.types'; -export const CompleteModal = ({ enrollment, events, programStages, setOpenCompleteModal, onUpdateStatus }: Props) => { +export const CompleteModal = ({ + enrollment, + events, + programStages, + program, + setOpenCompleteModal, + onUpdateStatus, +}: Props) => { const { fromClientDate } = useTimeZoneConversion(); const { programStagesWithActiveEvents, programStagesWithoutAccess } = useMemo( () => @@ -64,6 +71,7 @@ export const CompleteModal = ({ enrollment, events, programStages, setOpenComple ; events: Array<{ status: string; event: string; programStage: string }>; programStages: Array<{ name: string; id: string; access: { data: { write: boolean } } }>; + program: Record; setOpenCompleteModal: (open: boolean) => void; onUpdateStatus: (arg: Record, redirect?: boolean) => void; }; @@ -9,6 +10,7 @@ export type Props = { export type PlainProps = { programStagesWithActiveEvents: { [programId: string]: { name: string; count: number } }; programStagesWithoutAccess: { [programId: string]: { name: string; count: number } }; + program: Record; setOpenCompleteModal: (open: boolean) => void; onCompleteEnrollment: () => void; onCompleteEnrollmentAndEvents: () => void; 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..d88639544f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/Delete.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/Delete.component.tsx @@ -12,11 +12,16 @@ import { import i18n from '@dhis2/d2-i18n'; import type { Props } from './delete.types'; import { ConditionalTooltip } from '../../../Tooltips/ConditionalTooltip/'; +import { getTermLabelFromProgram, LabelKeys } from '../../../../metaData'; -export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Props) => { +export const Delete = ({ canCascadeDeleteEnrollment, enrollment, program, 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 } = getTermLabelFromProgram([LabelKeys.enrollmentSingular], { program }); + const tooltipContent = i18n.t( + 'You do not have access to delete this {{enrollmentLabel}}', + { enrollmentLabel }, + ); return ( @@ -37,11 +42,17 @@ export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Pro onClose={() => setToggle(false)} dataTest="widget-enrollment-actions-modal" > - {i18n.t('Delete enrollment')} + {i18n.t('Delete {{enrollmentLabel}}', { enrollmentLabel })} - {i18n.t('Are you sure you want to delete this enrollment?')} + {i18n.t( + 'Are you sure you want to delete this {{enrollmentLabel}}?', + { enrollmentLabel }, + )} {' '} - {i18n.t('This will permanently remove the current enrollment.')} + {i18n.t( + 'This will permanently remove the current {{enrollmentLabel}}.', + { enrollmentLabel }, + )} @@ -52,7 +63,7 @@ export const Delete = ({ canCascadeDeleteEnrollment, enrollment, onDelete }: Pro destructive onClick={() => onDelete(enrollment)} > - {i18n.t('Yes, delete enrollment.')} + {i18n.t('Yes, delete {{enrollmentLabel}}.', { enrollmentLabel })} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/delete.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/delete.types.ts index a4c90466f3..091b7d6ea5 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/delete.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Delete/delete.types.ts @@ -1,5 +1,6 @@ export type Props = { enrollment: Record; canCascadeDeleteEnrollment: boolean; + program: Record; onDelete: (arg: Record) => void; }; 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..7c327ca647 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/Followup.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/Followup.component.tsx @@ -1,10 +1,12 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { IconFlag16, MenuItem } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import type { Props } from './followup.types'; +import { getTermLabelFromProgram, LabelKeys } from '../../../../metaData'; -export const Followup = ({ enrollment, onUpdate }: Props) => - (enrollment.followUp ? ( +export const Followup = ({ enrollment, program, onUpdate }: Props) => { + const { followUpLabel } = getTermLabelFromProgram([LabelKeys.followUpSingular], { program }); + return enrollment.followUp ? ( }) } icon={} - label={i18n.t('Remove mark for follow-up')} + label={i18n.t('Remove mark for {{followUpLabel}}', { followUpLabel })} suffix="" /> ) : ( @@ -29,7 +31,8 @@ export const Followup = ({ enrollment, onUpdate }: Props) => }) } icon={} - label={i18n.t('Mark for follow-up')} + label={i18n.t('Mark for {{followUpLabel}}', { followUpLabel })} suffix="" /> - )); + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/followup.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/followup.types.ts index ba5acac97b..d606d8ec81 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/followup.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/Followup/followup.types.ts @@ -1,4 +1,5 @@ export type Props = { enrollment: Record; + program: Record; onUpdate: (arg: Record) => void; }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts index ab451af273..5d3f00e87d 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Actions/actions.types.ts @@ -5,6 +5,7 @@ export type Props = { enrollment: any; events: Array<{ status: string; event: string; programStage: string }>; programStages: Array<{ name: string; id: string; access: { data: { write: boolean } } }>; + program: Record; refetchEnrollment: QueryRefetchFunction; refetchTEI: QueryRefetchFunction; ownerOrgUnitId: string; @@ -26,6 +27,7 @@ export type PlainProps = { enrollment: any; events: Array<{ status: string; event: string; programStage: string }>; programStages: Array<{ name: string; id: string; access: { data: { write: boolean } } }>; + program: Record; ownerOrgUnitId: string; onUpdate: (arg: Record) => void; onUpdateStatus: (arg: Record, redirect?: boolean) => void; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx index b7aa937c9d..8f5c7d2702 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/Date/Date.component.tsx @@ -17,7 +17,7 @@ import { systemSettingsStore } from '../../../metaDataMemoryStores'; import { convertValue as convertValueClientToView } from '../../../converters/clientToView'; import { convertValue as convertValueFormToClient } from '../../../converters/formToClient'; import { convertValue as convertValueClientToServer } from '../../../converters/clientToServer'; -import { dataElementTypes } from '../../../metaData'; +import { dataElementTypes, getTermLabelFromProgram, LabelKeys } from '../../../metaData'; type OwnProps = { date: string; @@ -25,6 +25,7 @@ type OwnProps = { locale: string; readOnly: boolean; displayAutoGeneratedEventWarning: boolean; + program: Record; onSave: (date: string) => void; allowFutureDate?: boolean; }; @@ -93,6 +94,7 @@ const DateComponentPlain = ({ locale, readOnly, displayAutoGeneratedEventWarning, + program, onSave, allowFutureDate, classes, @@ -100,6 +102,10 @@ const DateComponentPlain = ({ const [editMode, setEditMode] = useState(false); const [selectedDate, setSelectedDate] = useState(); const [validation, setValidation] = useState<{ error: boolean; validationText: string }>(); + const { eventsLabel } = getTermLabelFromProgram( + [LabelKeys.eventPlural], + { program }, + ); const validateDate = useCallback((dateString: string, internalComponentError: any) => { if (!hasValue(dateString)) { @@ -194,7 +200,10 @@ const DateComponentPlain = ({
{displayAutoGeneratedEventWarning && (
- {i18n.t('Existing dates for auto-generated events will not be updated.')} + {i18n.t( + 'Existing dates for auto-generated {{eventsLabel}} will not be updated.', + { eventsLabel }, + )}
)}
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..f3ca6b6472 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 @@ -1,9 +1,10 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { cx } from '@emotion/css'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { colors, IconInfo16, IconWarning16 } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { useOrgUnitNameWithAncestors } from '../../../../metadataRetrieval/orgUnitName'; +import { getTermLabelFromProgram, LabelKeys } from '../../../../metaData'; import { OrgUnitScopes } from '../hooks/useTransferValidation'; import { ProgramAccessLevels } from '../hooks/useProgramAccessLevel'; @@ -11,6 +12,7 @@ type Props = { ownerOrgUnitId: string; validOrgUnitId?: string; programAccessLevel: string; + program: Record; orgUnitScopes: { origin: keyof typeof OrgUnitScopes | null; destination: keyof typeof OrgUnitScopes | null; @@ -44,10 +46,12 @@ const InfoBoxesPlain = ({ validOrgUnitId, programAccessLevel, orgUnitScopes, + program, classes, }: Props & WithStyles) => { const { displayName: ownerOrgUnitName } = useOrgUnitNameWithAncestors(ownerOrgUnitId); const { displayName: newOrgUnitName } = useOrgUnitNameWithAncestors(validOrgUnitId ?? null); + const { enrollmentLabel } = getTermLabelFromProgram([LabelKeys.enrollmentSingular], { program }); const showWarning = [ProgramAccessLevels.PROTECTED, ProgramAccessLevels.CLOSED].includes(programAccessLevel as any) && orgUnitScopes.destination === OrgUnitScopes.SEARCH; @@ -57,20 +61,26 @@ const InfoBoxesPlain = ({ {newOrgUnitName && (
- {i18n.t('Transferring enrollment ownership from {{ownerOrgUnit}} to {{newOrgUnit}}{{escape}}', { - ownerOrgUnit: ownerOrgUnitName, - newOrgUnit: newOrgUnitName, - escape: '.', - })} + {i18n.t( + 'Transferring {{enrollmentLabel}} ownership from {{ownerOrgUnit}} to {{newOrgUnit}}{{escape}}', + { + enrollmentLabel, + ownerOrgUnit: ownerOrgUnitName, + newOrgUnit: newOrgUnitName, + escape: '.', + }, + )}
)} {showWarning && (
- {i18n.t('You will lose access to the enrollment when transferring ownership to {{organisationUnit}}.', { - organisationUnit: newOrgUnitName, - })} + {i18n.t( + 'You will lose access to the {{enrollmentLabel}} ' + + 'when transferring ownership to {{organisationUnit}}.', + { enrollmentLabel, organisationUnit: newOrgUnitName }, + )}
)}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.component.tsx index 0ed8c66bc4..e2f326ba7a 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,14 +12,20 @@ import type { TransferModalProps } from './TransferModal.types'; import { OrgUnitField } from './OrgUnitField'; import { useTransferValidation } from './hooks/useTransferValidation'; import { InfoBoxes } from './InfoBoxes'; +import { getTermLabelFromProgram, LabelKeys } from '../../../metaData'; export const TransferModal = ({ enrollment, ownerOrgUnitId, + program, setOpenTransfer, onUpdateOwnership, isTransferLoading, }: TransferModalProps) => { + const { enrollmentLabel, orgUnitLabel } = getTermLabelFromProgram( + [LabelKeys.enrollmentSingular, LabelKeys.orgUnitSingular], + { program }, + ); const { selectedOrgUnit, handleOrgUnitChange, @@ -51,7 +57,10 @@ export const TransferModal = ({
- {i18n.t('Choose the organisation unit to which enrollment ownership should be transferred.')} + {i18n.t( + 'Choose the {{orgUnitLabel}} to which {{enrollmentLabel}} ownership should be transferred.', + { orgUnitLabel, enrollmentLabel }, + )}
diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.types.ts b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.types.ts index f09635cf61..1b11471aab 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/TransferModal/TransferModal.types.ts @@ -3,6 +3,7 @@ import type { UpdateEnrollmentOwnership } from '../Actions/Transfer/hooks/useUpd export type TransferModalProps = { enrollment: { program: string }; ownerOrgUnitId: string; + program: Record; setOpenTransfer: (toggle: boolean) => void; onUpdateOwnership: UpdateEnrollmentOwnership; isTransferLoading: boolean; diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx index 29b02e6023..141e44a1f9 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollment/WidgetEnrollment.component.tsx @@ -17,7 +17,7 @@ import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { PlainProps } from './enrollment.types'; import { Status } from './Status'; -import { dataElementTypes } from '../../metaData'; +import { dataElementTypes, getTermLabelFromProgram, LabelKeys } from '../../metaData'; import { convertValue } from '../../converters/clientToView'; import { useOrgUnitNameWithAncestors } from '../../metadataRetrieval/orgUnitName'; import { Date } from './Date'; @@ -53,7 +53,8 @@ const styles = { const getGeometryType = geometryType => (geometryType === 'Point' ? dataElementTypes.COORDINATE : dataElementTypes.POLYGON); -const getEnrollmentDateLabel = program => program.displayEnrollmentDateLabel ?? i18n.t('Enrollment date'); +const getEnrollmentDateLabel = (program, enrollmentLabel: string) => + program.displayEnrollmentDateLabel ?? i18n.t('{{enrollmentLabel}} date', { enrollmentLabel }); const getIncidentDateLabel = program => program.displayIncidentDateLabel ?? i18n.t('Incident date'); const WidgetEnrollmentPlain = ({ @@ -94,13 +95,18 @@ const WidgetEnrollmentPlain = ({ const orgUnitClientValue = { id: enrollment?.orgUnit, name: orgUnitName, ancestors }; const ownerOrgUnitClientValue = { id: ownerOrgUnit?.id, name: ownerOrgUnitName, ancestors: ownerAncestors }; + // Example use of getTermLabelFromProgram. + const { enrollmentLabel, followUpLabel } = getTermLabelFromProgram( + [LabelKeys.enrollmentSingular, LabelKeys.followUpSingular], + { program }, + ); return (
- {i18n.t('Enrollment')} + {i18n.t('{{enrollmentLabel}}', { enrollmentLabel })} {showWidgetBadge && (
{initError && (
- {i18n.t('Enrollment widget could not be loaded. Please try again later')} + {i18n.t( + '{{enrollmentLabel}} widget could not be loaded. Please try again later', + { enrollmentLabel }, + )}
)} {loading && } @@ -126,7 +135,7 @@ const WidgetEnrollmentPlain = ({
{enrollment.followUp && ( - {i18n.t('Follow-up')} + {i18n.t('{{followUpLabel}}', { followUpLabel })} )} @@ -135,10 +144,11 @@ const WidgetEnrollmentPlain = ({
@@ -152,6 +162,7 @@ const WidgetEnrollmentPlain = ({ locale={locale} readOnly={enrollmentReadOnly} displayAutoGeneratedEventWarning={displayAutoGeneratedEventWarning} + program={program} onSave={updateIncidentDate} allowFutureDate={program.selectIncidentDatesInFuture} /> @@ -206,6 +217,7 @@ const WidgetEnrollmentPlain = ({ tetName={program.trackedEntityType.displayName} onlyEnrollOnce={program.onlyEnrollOnce} programStages={program.programStages} + program={program} enrollment={enrollment} events={events} ownerOrgUnitId={ownerOrgUnit.id} diff --git a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts index 048ac5018c..bdf71d067a 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollment/hooks/useProgram.ts @@ -1,11 +1,27 @@ import { useMemo } from 'react'; import { useDataQuery } from '@dhis2/app-runtime'; +import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; type ProgramData = { featureType: string; [key: string]: any; }; +const baseFields = [ + 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,' + + 'displayEnrollmentLabel,displayFollowUpLabel,displayOrgUnitLabel,' + + 'displayRelationshipLabel,displayNoteLabel,displayTrackedEntityAttributeLabel,' + + 'displayProgramStageLabel,displayEventLabel,' + + 'trackedEntityType[displayName,access],' + + 'programStages[autoGenerateEvent,name,access,id],' + + 'access,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture', +]; + +const pluralFields = [ + 'displayEnrollmentsLabel,displayProgramStagesLabel,displayEventsLabel,' + + 'displayNotesLabel,displayRelationshipsLabel,displayTrackedEntityAttributesLabel', +]; + export const useProgram = (programId: string) => { const { error, loading, data } = useDataQuery( useMemo( @@ -13,15 +29,9 @@ export const useProgram = (programId: string) => { program: { resource: `programs/${programId}`, params: { - fields: [ - 'displayIncidentDate,displayIncidentDateLabel,displayEnrollmentDateLabel,onlyEnrollOnce,' + - 'displayEnrollmentLabel,displayEnrollmentsLabel,displayFollowUpLabel,displayOrgUnitLabel,' + - 'displayRelationshipLabel,displayNoteLabel,displayTrackedEntityAttributeLabel,' + - 'displayProgramStageLabel,displayProgramStagesLabel,displayEventLabel,displayEventsLabel,' + - 'trackedEntityType[displayName,access],' + - 'programStages[autoGenerateEvent,name,access,id],' + - 'access,featureType,selectEnrollmentDatesInFuture,selectIncidentDatesInFuture', - ], + fields: featureAvailable(FEATURES.customTerminologyPlurals) + ? [...baseFields, ...pluralFields] + : baseFields, }, }, }), diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx index 411a31e2e6..5ce359aeb4 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.component.tsx @@ -198,11 +198,11 @@ const buildOrgUnitSettingsFn = () => { getComponent: () => orgUnitComponent, getComponentProps: (props: any) => createComponentProps(props, { width: props && props.formHorizontal ? 150 : 350, - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, required: true, }), getPropName: () => 'orgUnit', - getValidatorContainers: () => getOrgUnitValidatorContainers(), + getValidatorContainers: (props: any) => getOrgUnitValidatorContainers(props.orgUnitLabel), getMeta: () => ({ placement: placements.TOP, section: dataEntrySectionNames.BASICINFO, @@ -308,13 +308,14 @@ const buildNotesSettingsFn = () => { const notesSettings = { getComponent: () => noteComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Notes'), + label: props.notesLabel, onAddNote: props.onAddNote, id: 'notes', dataEntryId: props.id, + noteLabel: props.noteLabel, }), getPropName: () => 'note', - getValidatorContainers: () => getNoteValidatorContainers(), + getValidatorContainers: (props: any) => getNoteValidatorContainers(props.eventLabel, props.noteLabel), getMeta: () => ({ placement: placements.BOTTOM, section: dataEntrySectionNames.NOTES, @@ -441,6 +442,8 @@ type Props = { placementDomNodeForSavingText?: HTMLElement; programName: string; orgUnitFieldValue?: OrgUnit | null; + notesLabel: string; + relationshipsLabel: string; }; type DataEntrySection = { @@ -448,35 +451,8 @@ type DataEntrySection = { name: string, }; -const dataEntrySectionDefinitions = { - [dataEntrySectionNames.BASICINFO]: { - placement: placements.TOP, - name: i18n.t('Basic info'), - }, - [dataEntrySectionNames.STATUS]: { - placement: placements.BOTTOM, - name: i18n.t('Status'), - }, - [dataEntrySectionNames.NOTES]: { - placement: placements.BOTTOM, - name: i18n.t('Notes'), - }, - [dataEntrySectionNames.RELATIONSHIPS]: { - placement: placements.BOTTOM, - name: i18n.t('Relationships'), - }, - [dataEntrySectionNames.ASSIGNEE]: { - placement: placements.BOTTOM, - name: i18n.t('Assignee'), - }, - [AOCsectionKey]: { - placement: placements.TOP, - name: '', - }, -}; class DataEntryPlain extends Component> { relationshipsInstance?: HTMLDivElement | null; - dataEntrySections: { [key: string]: DataEntrySection }; fieldOptions: { theme: any; fieldLabelMediaBasedClass?: string }; constructor(props: Props & WithStyles) { super(props); @@ -484,7 +460,6 @@ class DataEntryPlain extends Component> { theme: props.theme, fieldLabelMediaBasedClass: props.classes.fieldLabelMediaBased, }; - this.dataEntrySections = dataEntrySectionDefinitions; } componentDidMount() { @@ -516,9 +491,38 @@ class DataEntryPlain extends Component> { programName, stage, orgUnitFieldValue, + notesLabel, + relationshipsLabel, ...passOnProps } = this.props; + const dataEntrySections: { [key: string]: DataEntrySection } = { + [dataEntrySectionNames.BASICINFO]: { + placement: placements.TOP, + name: i18n.t('Basic info'), + }, + [dataEntrySectionNames.STATUS]: { + placement: placements.BOTTOM, + name: i18n.t('Status'), + }, + [dataEntrySectionNames.NOTES]: { + placement: placements.BOTTOM, + name: notesLabel, + }, + [dataEntrySectionNames.RELATIONSHIPS]: { + placement: placements.BOTTOM, + name: relationshipsLabel, + }, + [dataEntrySectionNames.ASSIGNEE]: { + placement: placements.BOTTOM, + name: i18n.t('Assignee'), + }, + [AOCsectionKey]: { + placement: placements.TOP, + name: '', + }, + }; + return (
{/* the props orgUnit, orgUnitId and selectedOrgUnitId should all be removed from here. See DHIS2-18869 */} @@ -527,8 +531,10 @@ class DataEntryPlain extends Component> { onUpdateFormField={onUpdateField} onUpdateFormFieldAsync={onStartAsyncUpdateField} fieldOptions={this.fieldOptions} - dataEntrySections={this.dataEntrySections} + dataEntrySections={dataEntrySections} relationshipsRef={this.setRelationshipsInstance} + notesLabel={notesLabel} + relationshipsLabel={relationshipsLabel} stage={stage} orgUnitIdFieldValue={orgUnitFieldValue?.id} orgUnit={orgUnitFieldValue} diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx index 5b1f9e9a42..85cac6b278 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/DataEntry.container.tsx @@ -15,6 +15,18 @@ import { import type { AddEventSaveType } from './addEventSaveTypes'; import type { ContainerProps } from './dataEntry.types'; import { useProgramExpiryForUser } from '../../../hooks'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; +import { LabelKeys } from '../../../metaData'; + +const customLabels = [ + LabelKeys.orgUnitSingular, + LabelKeys.eventSingular, + LabelKeys.noteSingular, + LabelKeys.notePlural, + LabelKeys.relationshipPlural, +] as const; + +const WrappedDataEntryComponent = withCustomLabels(customLabels)(DataEntryComponent); export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...passOnProps }: ContainerProps) => { const dispatch = useDispatch(); @@ -77,7 +89,7 @@ export const DataEntry = ({ rulesExecutionDependenciesClientFormatted, id, ...pa dispatch(setNewEventSaveTypes(newSaveTypes)); }, [dispatch]); return ( - !value; -export const getNoteValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateNote, - errorMessage: i18n.t('Please add or cancel the note before saving the event'), - }, - ]; - return validatorContainers; -}; +export const getNoteValidatorContainers = (eventLabel: string, noteLabel: string) => [ + { + validator: validateNote, + errorMessage: i18n.t('Please add or cancel the {{noteLabel}} before saving the {{eventLabel}}', { + eventLabel, + noteLabel, + }), + }, +]; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts index ec6fb14c72..4deebd3128 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts @@ -1,14 +1,11 @@ -import { isValidOrgUnit } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; +import { isValidOrgUnit } from 'capture-core-utils/validators/form'; const validateOrgUnit = (value?: any) => isValidOrgUnit(value); -export const getOrgUnitValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateOrgUnit, - errorMessage: i18n.t('Please provide an valid organisation unit'), - }, - ]; - return validatorContainers; -}; +export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ + { + validator: validateOrgUnit, + errorMessage: i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + }, +]; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts index ba320d2c79..2afdb8fde7 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/helpers/getOpenDataEntryActions.ts @@ -5,8 +5,13 @@ import { getNoteValidatorContainers } from '../fieldValidators/note.validatorCon import type { ProgramCategory } from '../../../WidgetEventSchedule/CategoryOptions/CategoryOptions.types'; import { getCategoryOptionsValidatorContainers } from '../fieldValidators/categoryOptions.validatorContainersGetter'; import type { DataEntryPropToInclude } from '../../../DataEntry/actions/dataEntryLoad.utils'; +import { getTermLabel, LabelKeys } from '../../../../metaData'; -const dataEntryPropsToInclude: Array = [ +const buildDataEntryPropsToInclude = ( + orgUnitLabel: string, + eventLabel: string, + noteLabel: string, +): Array => [ { id: 'occurredAt', type: 'DATE', @@ -15,7 +20,7 @@ const dataEntryPropsToInclude: Array = [ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(orgUnitLabel), }, { id: 'scheduledAt', @@ -30,7 +35,7 @@ const dataEntryPropsToInclude: Array = [ { id: 'note', type: 'TEXT', - validatorContainers: getNoteValidatorContainers(), + validatorContainers: getNoteValidatorContainers(eventLabel, noteLabel), clientIgnore: true, }, { @@ -40,12 +45,27 @@ const dataEntryPropsToInclude: Array = [ ]; export const getOpenDataEntryActions = - (dataEntryId: string, itemId: string, programCategory?: ProgramCategory, orgUnit?: Record) => { + ( + dataEntryId: string, + itemId: string, + programId: string, + programCategory?: ProgramCategory, + orgUnit?: Record, + ) => { const defaultDataEntryValues = { orgUnit: orgUnit ? { id: orgUnit.id, name: orgUnit.name, path: orgUnit.path } : undefined, }; + const { orgUnitLabel, eventLabel, noteLabel } = getTermLabel( + [LabelKeys.orgUnitSingular, LabelKeys.eventSingular, LabelKeys.noteSingular], + { programId }, + ); + const dataEntryPropsToInclude = buildDataEntryPropsToInclude( + orgUnitLabel, + eventLabel, + noteLabel, + ); if (programCategory && programCategory.categories) { dataEntryPropsToInclude.push(...programCategory.categories.map(category => ({ id: `attributeCategoryOptions-${category.id}`, diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx index bf5f1d4d89..ca223adbdf 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/OrgUnitFetcher/OrgUnitFetcher.component.tsx @@ -1,19 +1,21 @@ -import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import React from 'react'; import { useCoreOrgUnit } from '../../../metadataRetrieval/coreOrgUnit'; import { Validated } from '../Validated/Validated.container'; import type { OrgUnitFetcherProps } from './orgUnitFetcher.types'; +import { LabelKeys, useTermLabel } from '../../../metaData'; export const OrgUnitFetcher = ({ orgUnitId, ...passOnProps }: OrgUnitFetcherProps) => { const { error, orgUnit } = useCoreOrgUnit(orgUnitId); + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular], { programId: passOnProps.program?.id }); if (error) { return (
- {i18n.t('organisation unit could not be retrieved. Please try again later.')} + {i18n.t('{{orgUnitLabel}} could not be retrieved. Please try again later.', { orgUnitLabel })}
); } diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts index 9768ffca43..c41bce14fe 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/Validated/useLifecycle.ts @@ -38,7 +38,7 @@ export const useLifecycle = ({ useEffect(() => { if (!isLoading) { dispatch(batchActions([ - ...getOpenDataEntryActions(dataEntryId, itemId, programCategory, orgUnitContext), + ...getOpenDataEntryActions(dataEntryId, itemId, program.id, programCategory, orgUnitContext), ])); dataEntryReadyRef.current = true; delayRulesExecutionRef.current = true; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx index 4e48426865..48c0f609d0 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/WidgetEnrollmentEventNew.container.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; -import { getProgramAndStageForProgram, TrackerProgram } from '../../metaData'; +import { getProgramAndStageForProgram, LabelKeys, TrackerProgram, useTermLabel } from '../../metaData'; import { OrgUnitFetcher } from './OrgUnitFetcher/OrgUnitFetcher.container'; import type { WidgetProps } from './WidgetEnrollmentEventNew.types'; import { useMetadataForProgramStage } from '../DataEntries/common/ProgramStage/useMetadataForProgramStage'; @@ -12,6 +12,7 @@ export const WidgetEnrollmentEventNew = ({ ...passOnProps }: WidgetProps) => { const { program } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular], { programId, stageId }); const { stage, formFoundation, @@ -30,7 +31,7 @@ export const WidgetEnrollmentEventNew = ({ if (!program || !stage || !(program instanceof TrackerProgram) || isError || !formFoundation) { return (
- {i18n.t('program or stage is invalid')} + {i18n.t('Program or {{programStageLabel}} is invalid', { programStageLabel })}
); } 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..cddf0b6bd4 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEnrollmentNote/WidgetEnrollmentNote.component.tsx @@ -1,11 +1,12 @@ -import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { requestAddNoteForEnrollment } from './WidgetEnrollmentNote.actions'; import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import { useLocationQuery } from '../../utils/routing'; +import { LabelKeys, useTermLabel } from '../../metaData'; export const WidgetEnrollmentNote = () => { const dispatch = useDispatch(); @@ -17,6 +18,9 @@ export const WidgetEnrollmentNote = () => { trackedEntityTypeName, showWidgetBadge, } = useEnrollmentAccessContext(); + const { enrollmentLabel, noteLabel, notesLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.noteSingular, LabelKeys.notePlural], + ); const onAddNote = (newNoteValue: string) => { dispatch(requestAddNoteForEnrollment(enrollmentId, newNoteValue)); @@ -25,9 +29,16 @@ export const WidgetEnrollmentNote = () => { return (
, programCategory?: ProgramCategory }) => { + const { orgUnitLabel } = getTermLabel([LabelKeys.orgUnitSingular], { programId: program.id }); const dataEntryPropsToInclude = [ { id: 'occurredAt', @@ -119,7 +120,7 @@ export const openEventForEditInDataEntry = ({ { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(orgUnitLabel), }, { clientId: 'geometry', @@ -165,7 +166,10 @@ export const openEventForEditInDataEntry = ({ if (program instanceof TrackerProgram) { const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { - throw Error(i18n.t('stage not found in rules execution')); + const { programStageLabel } = getTermLabel([LabelKeys.programStageSingular], { programId: program.id }); + throw Error(i18n.t('{{programStageLabel}} not found in rules execution', { + programStageLabel, + })); } // TODO: Add attributeValues & enrollmentData effects = getApplicableRuleEffectsForTrackerProgram({ diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts index cd0f06fee8..6aa2e15766 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/epics/editEventDataEntry.epics.ts @@ -9,7 +9,7 @@ import { batchActionTypes as editEventDataEntryBatchActionTypes, actionTypes as editEventDataEntryActionTypes, } from '../editEventDataEntry.actions'; -import { getProgramThrowIfNotFound, dataElementTypes } from '../../../../metaData'; +import { getProgramThrowIfNotFound, dataElementTypes, getTermLabel, LabelKeys } from '../../../../metaData'; import { convertValue } from '../../../../converters/serverToClient'; import { getCurrentClientValues, @@ -56,7 +56,10 @@ const runRulesForEditSingleEvent = async ({ : getStageFromEvent(event)?.stage; if (!stage) { - throw Error(i18n.t('stage not found in rules execution')); + const { programStageLabel } = getTermLabel([LabelKeys.programStageSingular], { programId }); + throw Error(i18n.t('{{programStageLabel}} not found in rules execution', { + programStageLabel, + })); } const foundation = stage.stageForm; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts index 7513b5c7b7..25753aed58 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/fieldValidators/orgUnit.validatorContainersGetter.ts @@ -1,14 +1,11 @@ -import { isValidOrgUnit } from 'capture-core-utils/validators/form'; import i18n from '@dhis2/d2-i18n'; +import { isValidOrgUnit } from 'capture-core-utils/validators/form'; const validateOrgUnit = (value?: Record) => isValidOrgUnit(value); -export const getOrgUnitValidatorContainers = () => { - const validatorContainers = [ - { - validator: validateOrgUnit, - errorMessage: i18n.t('Please provide an valid organisation unit'), - }, - ]; - return validatorContainers; -}; +export const getOrgUnitValidatorContainers = (orgUnitLabel: string) => [ + { + validator: validateOrgUnit, + errorMessage: i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), + }, +]; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx index 0bdbbbce42..a88d6105b6 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import i18n from '@dhis2/d2-i18n'; import { Modal, ModalTitle, ModalContent, ModalActions, ButtonStrip, Button } from '@dhis2/ui'; import type { Props, State } from './withDeleteButton.types'; +import { getTermLabel, LabelKeys } from '../../../metaData'; const getDeleteButton = (InnerComponent: React.ComponentType) => class DeleteButtonHOC extends React.Component { @@ -17,52 +18,58 @@ const getDeleteButton = (InnerComponent: React.ComponentType) => return this.innerInstance; } - renderDeleteButton = (hasDeleteButton?: boolean) => ( - hasDeleteButton ? (
- - {this.state.isOpen && ( - { + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: this.props.programId }); + return ( + hasDeleteButton ? (
+ - - - - - )} -
) : null - ); + {i18n.t('Delete')} + + {this.state.isOpen && ( + + + {i18n.t('Delete {{eventLabel}}', { eventLabel })} + + + {i18n.t( + 'Deleting an {{eventLabel}} is permanent and cannot be undone.', + { eventLabel }, + )} + {' '} + {i18n.t('Are you sure you want to delete this {{eventLabel}}? ', { eventLabel })} + + + + + + + + + )} +
) : null + ); + }; render() { const { onDelete, hasDeleteButton, ...passOnProps } = this.props; diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts index a1d1462bbd..4f4de11f7e 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/DataEntry/withDeleteButton.types.ts @@ -5,6 +5,7 @@ export type Props = { formHorizontal?: boolean; formFoundation: RenderFoundation; hasDeleteButton?: boolean; + programId: string; }; export type State = { diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx index a14687d4aa..9537ade161 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.component.tsx @@ -166,10 +166,17 @@ const buildScheduleDateSettingsFn = () => { const isScheduleableStatus = [statusTypes.SCHEDULE, statusTypes.OVERDUE].includes(innerProps.eventStatus); + const eventLabel = innerProps.eventLabel; + const eventsLabel = innerProps.eventsLabel; return isScheduleableStatus ? - i18n.t('Go to “Schedule” tab to reschedule this event') : - i18n.t('Scheduled date cannot be changed for {{ eventStatus }} events', - { eventStatus: translatedStatusTypes()[innerProps.eventStatus] }); + i18n.t( + 'Go to “Schedule” tab to reschedule this {{eventLabel}}', + { eventLabel }, + ) : + i18n.t( + 'Scheduled date cannot be changed for {{ eventStatus }} {{eventsLabel}}', + { eventStatus: translatedStatusTypes()[innerProps.eventStatus], eventsLabel }, + ); }, })( withDisplayMessages()( @@ -225,11 +232,11 @@ const buildOrgUnitSettingsFn = () => { getComponent: () => orgUnitComponent, getComponentProps: (props: any) => createComponentProps(props, { width: props && props.formHorizontal ? 150 : 350, - label: i18n.t('Organisation unit'), + label: props.orgUnitLabel, required: true, }), getPropName: () => 'orgUnit', - getValidatorContainers: () => getOrgUnitValidatorContainers(), + getValidatorContainers: (props: any) => getOrgUnitValidatorContainers(props.orgUnitLabel), getMeta: () => ({ placement: placements.TOP, section: dataEntrySectionNames.BASICINFO, @@ -328,7 +335,10 @@ const buildCompleteFieldSettingsFn = () => { const canUncompleteEvent = props.canUncompleteEvent; const shouldDisable = isEventCompleted && !canUncompleteEvent; return shouldDisable - ? i18n.t('You do not have access to uncomplete this event') + ? i18n.t( + 'You do not have access to uncomplete this {{eventLabel}}', + { eventLabel: props.eventLabel }, + ) : undefined; })(TrueOnlyField), ), @@ -346,7 +356,7 @@ const buildCompleteFieldSettingsFn = () => { const shouldDisable = isEventCompleted && !canUncompleteEvent; return createComponentProps(props, { - label: i18n.t('Complete event'), + label: i18n.t('Complete {{eventLabel}}', { eventLabel: props.eventLabel }), id: 'complete', disabled: shouldDisable, eventStatus: props.eventStatus, diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts index e3accd78a5..8eed8b2510 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/EditEventDataEntry.container.ts @@ -7,13 +7,13 @@ import type { OrgUnit } from '@dhis2/rules-engine-javascript'; import type { ReduxAction } from 'capture-core-utils/types'; import { EditEventDataEntryComponent } from './EditEventDataEntry.component'; import { withLoadingIndicator } from '../../../HOC/withLoadingIndicator'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; import { startAsyncUpdateFieldForEditEvent, startRunRulesOnUpdateForEditSingleEvent, batchActionTypes, } from '../DataEntry/editEventDataEntry.actions'; -import type { RenderFoundation } from '../../../metaData'; - +import { LabelKeys, type RenderFoundation } from '../../../metaData'; import { setCurrentDataEntry, startRunRulesPostUpdateField, } from '../../DataEntry/actions/dataEntry.actions'; @@ -24,9 +24,14 @@ import { startCreateNewAfterCompleting, requestSaveAndCompleteEnrollment, } from './editEventDataEntry.actions'; - import { getLocationQuery } from '../../../utils/routing/getLocationQuery'; +const customLabels = [ + LabelKeys.orgUnitSingular, + LabelKeys.eventSingular, + LabelKeys.eventPlural, +] as const; + const mapStateToProps = (state: any, props: any) => { const eventDetailsSection = state.viewEventPage.eventDetailsSection || {}; const itemId = state.dataEntries[props.dataEntryId] && state.dataEntries[props.dataEntryId].itemId; @@ -158,5 +163,5 @@ const mapDispatchToProps = (dispatch: any, props: any): any => ({ }); export const EditEventDataEntry = connect(mapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(EditEventDataEntryComponent), + withLoadingIndicator()(withCustomLabels(customLabels)(EditEventDataEntryComponent)), ); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts index e88dc48eb8..cadf4b0b8f 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.actions.ts @@ -67,7 +67,7 @@ export const prerequisitesErrorLoadingEditEventDataEntry = (message: string) => export const requestDeleteEventDataEntry = ({ eventId, enrollmentId }: { eventId: string; enrollmentId: string }) => actionCreator(actionTypes.REQUEST_DELETE_EVENT_DATA_ENTRY)({ eventId, enrollmentId }); -export const startDeleteEventDataEntry = (serverData: any, eventId: string, params: any) => +export const startDeleteEventDataEntry = (serverData: any, eventId: string, params: any, programId: string) => actionCreator(actionTypes.START_DELETE_EVENT_DATA_ENTRY)({ eventId }, { offline: { effect: { @@ -81,7 +81,7 @@ export const startDeleteEventDataEntry = (serverData: any, eventId: string, para }, rollback: { type: actionTypes.DELETE_EVENT_DATA_ENTRY_FAILED, - meta: { eventId, params }, + meta: { eventId, params, programId }, }, }, }); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts index dc613b0aa5..26d581e641 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/EditEventDataEntry/editEventDataEntry.epics.ts @@ -55,8 +55,8 @@ export const loadEditEventDataEntryEpic = (action$: any, store: ReduxStore) => const loadedValues = state.viewEventPage.loadedValues; const eventContainer = loadedValues.eventContainer; const metadataContainer = getProgramAndStageFromEvent(eventContainer.event); - if (metadataContainer.error) { - return prerequisitesErrorLoadingEditEventDataEntry(metadataContainer.error); + if (metadataContainer.error || !metadataContainer.program) { + return prerequisitesErrorLoadingEditEventDataEntry(metadataContainer.error ?? ''); } const program = metadataContainer.program; @@ -208,15 +208,16 @@ export const saveEditedEventFailedEpic = (action$: any, store: any) => return batchActions(actions, batchActionTypes.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED); })); -export const requestDeleteEventDataEntryEpic = (action$: any, store: ReduxStore, dependencies: any) => +export const requestDeleteEventDataEntryEpic = (action$: any, store: any, dependencies: any) => action$.pipe( ofType(actionTypes.REQUEST_DELETE_EVENT_DATA_ENTRY), map((action: any) => { const { eventId, enrollmentId } = action.payload; const params = { enrollmentId }; const serverData = { events: [{ event: eventId }] }; + const { programId } = store.value.enrollmentPage; dependencies.navigate(`/enrollment?${buildUrlQueryString(params)}`); - return startDeleteEventDataEntry(serverData, eventId, params); + return startDeleteEventDataEntry(serverData, eventId, params, programId); })); export const startCreateNewAfterCompletingEpic = ( diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx index 42edee90ce..1a09294f11 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.component.tsx @@ -147,7 +147,8 @@ const buildOrgUnitSettingsFn = () => { const orgUnitSettings = { getComponent: () => viewModeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Organisation unit'), + // Example use of withCustomLabels. + label: props.orgUnitLabel, valueConverter: value => dataElement.convertValue(value, valueConvertFn), }), getPropName: () => 'orgUnit', @@ -223,7 +224,8 @@ const buildCompleteFieldSettingsFn = () => { const completeSettings = { getComponent: () => viewModeComponent, getComponentProps: (props: any) => createComponentProps(props, { - label: i18n.t('Event completed'), + // Example use of withCustomLabels. + label: i18n.t('{{eventLabel}} completed', { eventLabel: props.eventLabel }), id: dataElement.id, valueConverter: value => dataElement.convertValue(value, valueConvertFn), }), @@ -283,6 +285,7 @@ type Props = { dataEntryId: string; programId: string; itemId: string; + notesLabel: string; }; type DataEntrySection = { @@ -290,28 +293,8 @@ type DataEntrySection = { name?: string; }; -const dataEntrySectionDefinitions = { - [dataEntrySectionNames.BASICINFO]: { - placement: placements.TOP, - name: i18n.t('Basic info'), - }, - [dataEntrySectionNames.STATUS]: { - placement: placements.BOTTOM, - name: i18n.t('Status'), - }, - [dataEntrySectionNames.NOTES]: { - placement: placements.BOTTOM, - name: i18n.t('Notes'), - }, - [AOCsectionKey]: { - placement: placements.TOP, - name: '', - }, -}; - class ViewEventDataEntryPlain extends Component> { fieldOptions: { theme: any; fieldLabelMediaBasedClass: string }; - dataEntrySections: { [key: string]: DataEntrySection }; constructor(props: Props & WithStyles) { super(props); @@ -319,7 +302,6 @@ class ViewEventDataEntryPlain extends Component ); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts index c9ecd0d685..41642b64ef 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/ViewEventDataEntry.container.ts @@ -1,7 +1,11 @@ import { connect } from 'react-redux'; import { ViewEventDataEntryComponent } from './ViewEventDataEntry.component'; import { withLoadingIndicator } from '../../../HOC/withLoadingIndicator'; +import { withCustomLabels } from '../../../HOC/withCustomLabels'; +import { LabelKeys } from '../../../metaData'; +// Example use of withCustomLabels. +const customLabels = [LabelKeys.orgUnitSingular, LabelKeys.eventSingular] as const; const mapStateToProps = (state: any, props: any) => { const eventDetailsSection = state.viewEventPage.eventDetailsSection || {}; @@ -20,5 +24,5 @@ const mapStateToProps = (state: any, props: any) => { const mapDispatchToProps = (): any => ({}); export const ViewEventDataEntry = connect(mapStateToProps, mapDispatchToProps)( - withLoadingIndicator()(ViewEventDataEntryComponent), + withLoadingIndicator()(withCustomLabels(customLabels)(ViewEventDataEntryComponent)), ); diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts index f01c953420..767db399a8 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventEdit/ViewEventDataEntry/viewEventDataEntry.actions.ts @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n'; import { type OrgUnit, effectActions } from '@dhis2/rules-engine-javascript'; import { actionCreator } from '../../../actions/actions.utils'; import type { RenderFoundation, Program } from '../../../metaData'; +import { getTermLabel, LabelKeys, dataElementTypes } from '../../../metaData'; import { getConvertGeometryIn, convertGeometryOut, convertStatusOut } from '../../DataEntries'; import { getDataEntryKey } from '../../DataEntry/common/getDataEntryKey'; import { loadEditDataEntryAsync } from '../../DataEntry/templates/dataEntryLoadEdit.template'; @@ -11,7 +12,6 @@ import { updateRulesEffects, filterApplicableRuleEffects, } from '../../../rules'; -import { dataElementTypes } from '../../../metaData'; import { convertClientToForm } from '../../../converters'; import type { ClientEventContainer } from '../../../events/eventRequests'; import { TrackerProgram, EventProgram } from '../../../metaData/Program'; @@ -60,6 +60,7 @@ export const loadViewEventDataEntry = attributeValues?: Array; onCategoriesQuery?: Promise | null; }) => { + const { orgUnitLabel } = getTermLabel([LabelKeys.orgUnitSingular], { programId: program.id }); const dataEntryPropsToInclude = [ { id: 'occurredAt', @@ -73,7 +74,7 @@ export const loadViewEventDataEntry = { id: 'orgUnit', type: 'ORGANISATION_UNIT', - validatorContainers: getOrgUnitValidatorContainers(), + validatorContainers: getOrgUnitValidatorContainers(orgUnitLabel), }, { clientId: 'geometry', @@ -145,7 +146,10 @@ export const loadViewEventDataEntry = if (program instanceof TrackerProgram) { const stage = getStageFromEvent(eventContainer.event)?.stage; if (!stage) { - throw Error(i18n.t('stage not found in rules execution')); + const { programStageLabel } = getTermLabel([LabelKeys.programStageSingular], { programId: program?.id }); + throw Error(i18n.t('{{programStageLabel}} not found in rules execution', { + programStageLabel, + })); } effects = getApplicableRuleEffectsForTrackerProgram({ diff --git a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx index f6ab983675..6a4935c7ec 100644 --- a/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventEdit/WidgetEventEdit.container.tsx @@ -152,6 +152,7 @@ const WidgetEventEditPlain = ({ > = { icon: { @@ -47,6 +48,7 @@ const WidgetHeaderPlain = ({ const { programCategory } = useCategoryCombinations(programId); const { icon, name } = stage; + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId, stageId: stage.id }); return ( <> @@ -73,7 +75,7 @@ const WidgetHeaderPlain = ({ onClick={() => dispatch(startShowEditEventDataEntry(orgUnit, programCategory))} data-test="widget-enrollment-event-edit-button" > - {i18n.t('Edit event')} + {i18n.t('Edit {{eventLabel}}', { eventLabel })} )} diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts index 7e62d9c502..8c1c2c216d 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.actions.ts @@ -13,14 +13,15 @@ export const batchActionTypes = { ADD_NOTE_BATCH_FOR_EVENT: 'AddNoteBatchForEvent', }; -export const requestAddNoteForEvent = (itemId: string, dataEntryId: string, note: string) => - actionCreator(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT)({ itemId, dataEntryId, note }); +export const requestAddNoteForEvent = (itemId: string, dataEntryId: string, note: string, programId: string) => + actionCreator(actionTypes.REQUEST_ADD_NOTE_FOR_EVENT)({ itemId, dataEntryId, note, programId }); export const startAddNoteForEvent = ( eventUid: string, serverData: Record, selections: Record, context: Record, + programId: string, ) => actionCreator(actionTypes.START_ADD_NOTE_FOR_EVENT)({ selections, context }, { offline: { @@ -32,6 +33,9 @@ export const startAddNoteForEvent = ( data: serverData, }, commit: { type: actionTypes.NOTE_ADDED_FOR_EVENT, meta: { selections, context } }, - rollback: { type: actionTypes.ADD_NOTE_FAILED_FOR_EVENT, meta: { selections, context } }, + rollback: { + type: actionTypes.ADD_NOTE_FAILED_FOR_EVENT, + meta: { selections, context, programId }, + }, }, }); diff --git a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx index d2306069a7..2a5f61847b 100644 --- a/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventNote/WidgetEventNote.component.tsx @@ -1,13 +1,14 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import i18n from '@dhis2/d2-i18n'; import type { Props } from './WidgetEventNote.types'; import { requestAddNoteForEvent } from './WidgetEventNote.actions'; import { WidgetNote } from '../WidgetNote'; import { ReadOnlyBadge } from '../ReadOnlyBadge'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { LabelKeys, useTermLabel } from '../../metaData'; -export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { +export const WidgetEventNote = ({ dataEntryKey, dataEntryId, programId }: Props) => { const dispatch = useDispatch(); const notes = useSelector(({ dataEntriesNotes }: { dataEntriesNotes: Record }) => dataEntriesNotes[`${dataEntryId}-${dataEntryKey}`] ?? []); @@ -16,17 +17,28 @@ export const WidgetEventNote = ({ dataEntryKey, dataEntryId }: Props) => { trackedEntityTypeName, showWidgetBadge, } = useEnrollmentAccessContext(); + const { eventLabel, noteLabel, notesLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.noteSingular, LabelKeys.notePlural], + { programId }, + ); const onAddNote = (newNoteValue: string) => { - dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue)); + dispatch(requestAddNoteForEvent(dataEntryKey, dataEntryId, newNoteValue, programId)); }; return (
{ + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + { programId, stageId }, + ); if (!scheduleDate || !suggestedScheduleDate) { return null; } @@ -58,17 +65,20 @@ const InfoBoxPlain = ({ {!!orgUnitName && ( <> {' '} - {i18n.t('There are {{count}} scheduled event in this program in {{orgUnitName}} on this day.', { - count: eventCountInOrgUnit, - orgUnitName, - // eslint-disable-next-line max-len - defaultValue: 'There are {{count}} scheduled event in this program in {{orgUnitName}} on this day.', - // eslint-disable-next-line max-len - defaultValue_plural: 'There are {{count}} scheduled events in this program in {{orgUnitName}} on this day.', - interpolation: { - escapeValue: false, + {i18n.t( + 'There are {{count}} scheduled {{eventLabel}} in this program ' + + 'in {{orgUnitName}} on this day.', + { + count: eventCountInOrgUnit, + orgUnitName, + eventLabel, + eventsLabel, + defaultValue: 'There are {{count}} scheduled {{eventLabel}} in this program ' + + 'in {{orgUnitName}} on this day.', + defaultValue_plural: 'There are {{count}} scheduled {{eventsLabel}} in this program ' + + 'in {{orgUnitName}} on this day.', }, - })} + )} )} diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.types.ts b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.types.ts index 6333ff7c52..23b063a651 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/InfoBox/InfoBox.types.ts @@ -4,4 +4,6 @@ export type PlainProps = { eventCountInOrgUnit: number; orgUnitName?: string | null; hideDueDate?: boolean; + programId: string; + stageId: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx index e27abe562d..3939e5e6e6 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleDate/ScheduleDate.component.tsx @@ -70,6 +70,8 @@ const ScheduleDatePlain = ({ classes, hideDueDate, expiryPeriod, + programId, + stageId, }: Props) => { const validateDate = (dateString: string, internalComponentError: any) => { if (!hasValue(dateString)) { @@ -100,11 +102,11 @@ const ScheduleDatePlain = ({ if (!isWithinValidPeriod) { return { error: true, - // eslint-disable-next-line max-len - validationText: i18n.t('The date entered belongs to an expired period. Enter a date after {{firstValidDate}}.', { - firstValidDate, - interpolation: { escapeValue: false }, - }), + validationText: i18n.t( + 'The date entered belongs to an expired period. ' + + 'Enter a date after {{firstValidDate}}.', + { firstValidDate, interpolation: { escapeValue: false } }, + ), }; } @@ -146,6 +148,8 @@ const ScheduleDatePlain = ({ eventCountInOrgUnit={eventCountInOrgUnit} orgUnitName={orgUnit?.name} hideDueDate={hideDueDate} + programId={programId} + stageId={stageId} />
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..decad1093e 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,5 +1,5 @@ -import React, { useState } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { useState } from 'react'; import { isValidOrgUnit } from 'capture-core-utils/validators/form'; import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; @@ -10,6 +10,7 @@ import { withInternalChangeHandler, withLabel, } from '../../FormFields/New'; +import { LabelKeys, useTermLabel } from '../../../metaData'; type OrgUnitValue = { checked: boolean; @@ -45,6 +46,7 @@ export const ScheduleOrgUnit = ({ orgUnit, }: Props) => { const [touched, setTouched] = useState(false); + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); const handleSelect = (event: any) => { setTouched(true); @@ -57,11 +59,11 @@ export const ScheduleOrgUnit = ({ }; const shouldShowError = (!isValidOrgUnit(orgUnit) && touched); - const errorMessages = i18n.t('Please provide a valid organisation unit'); + const errorMessages = i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }); 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, programId, stageId }: Props) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId, stageId }); + return ( + + + {orgUnitName + ? i18n.t( + 'Scheduling an {{eventLabel}} in {{stageName}} for {{programName}} in {{orgUnitName}}', + { orgUnitName, stageName, programName, eventLabel }, + ) + : i18n.t( + 'Scheduling an {{eventLabel}} in {{stageName}} for {{programName}}', + { stageName, programName, eventLabel }, + )} + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/scheduleText.types.ts b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/scheduleText.types.ts index 284e634d9d..bfb355986f 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/scheduleText.types.ts +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/ScheduleText/scheduleText.types.ts @@ -2,4 +2,6 @@ export type Props = { orgUnitName?: string; stageName: string; programName: string; + programId: string; + stageId: string; }; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.tsx index d3e3fb6a74..46b3e41103 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 { LabelKeys, useTermLabel } from '../../metaData'; const styles = (theme: any) => ({ wrapper: { @@ -73,6 +74,10 @@ const WidgetEventSchedulePlain = ({ const formIsValid = () => Boolean(isValidOrgUnit(orgUnit) && scheduleDate && !validation?.error); setIsFormValid(formIsValid()); }, [orgUnit, scheduleDate, validation, setIsFormValid]); + const { eventLabel, noteLabel, notesLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.noteSingular, LabelKeys.notePlural], + { programId, stageId }, + ); return ( } {enableUserAssignment && ( @@ -145,6 +157,8 @@ const WidgetEventSchedulePlain = ({ programName={programName} stageName={stageName} orgUnitName={orgUnit?.name || ''} + programId={programId} + stageId={stageId} />
diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx index 2b99d00290..e5a97692d0 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.tsx @@ -1,10 +1,10 @@ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { useTimeZoneConversion } from '@dhis2/app-runtime'; import moment from 'moment'; import { pipe } from 'capture-core-utils'; -import { getProgramAndStageForProgram, TrackerProgram, dataElementTypes } from '../../metaData'; +import { getProgramAndStageForProgram, TrackerProgram, dataElementTypes, LabelKeys, useTermLabel } from '../../metaData'; import { getCachedOrgUnitName } from '../../metadataRetrieval/orgUnitName'; import { useLocationQuery } from '../../utils/routing'; import { CurrentUser } from '../../utils/userInfo/CurrentUser'; @@ -38,6 +38,7 @@ export const WidgetEventSchedule = ({ ...passOnProps }: ContainerProps) => { const { program, stage } = useMemo(() => getProgramAndStageForProgram(programId, stageId), [programId, stageId]); + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular], { programId, stageId }); const dispatch = useDispatch(); const { programStageScheduleConfig }: {programStageScheduleConfig?: any} = useScheduleConfigFromProgramStage(stageId); const { programConfig }: {programConfig?: any} = useScheduleConfigFromProgram(programId); @@ -181,7 +182,7 @@ export const WidgetEventSchedule = ({ if (!program || !stage || !(program instanceof TrackerProgram) || !programStageScheduleConfig) { return (
- {i18n.t('Program or stage is invalid')} + {i18n.t('Program or {{programStageLabel}} is invalid', { programStageLabel })}
); } 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..9c0f66c138 100644 --- a/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx +++ b/src/core_modules/capture-core/components/WidgetNote/NoteSection/NoteSection.tsx @@ -71,6 +71,7 @@ const NoteSectionPlain = ({ notes, handleAddNote, readOnly, + noteLabel, classes, }: Props) => { const [isEditing, setEditing] = useState(false); @@ -158,7 +159,7 @@ const NoteSectionPlain = ({ primary small > - {i18n.t('Save note')} + {i18n.t('Save {{noteLabel}}', { noteLabel })} - {modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( - - )} - {(modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( - - )} - - } - > - {i18n.t( - 'Change information about this {{trackedEntityName}} here.', - { trackedEntityName, interpolation: { escapeValue: false } }, - )} - {' '} - {i18n.t('Information about this enrollment can be edited in the Enrollment widget.')} - - - -); + {modalState === TEI_MODAL_STATE.OPEN_DISABLE && ( + + )} + {(modalState === TEI_MODAL_STATE.OPEN || modalState === TEI_MODAL_STATE.OPEN_ERROR) && ( + + )} + + } + > + {i18n.t( + 'Change information about this {{trackedEntityName}} here.', + { trackedEntityName, interpolation: { escapeValue: false } }, + )} + {' '} + {i18n.t( + 'Information about this {{enrollmentLabel}} can be edited in the {{enrollmentLabel}} widget.', + { enrollmentLabel }, + )} + + + + ); +}; diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx index 5c547e8a55..716b5f87a7 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/DataEntry.container.tsx @@ -178,6 +178,7 @@ export const DataEntry = ({ saveAttempted={saveAttempted} trackedEntityName={trackedEntityName} formFoundation={formFoundation} + program={programAPI} onUpdateFormField={onUpdateFormField} onUpdateFormFieldAsync={onUpdateFormFieldAsync} modalState={modalState} diff --git a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts index e7bbe7c132..01c68f128f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/DataEntry/dataEntry.types.ts @@ -9,6 +9,7 @@ export type PlainProps = { trackedEntityName: string; saveAttempted: boolean; formFoundation: any; + program: Record; onCancel: () => void; onSave: () => void; onUpdateFormField: (innerAction: any) => void; 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 8f7b78883c..0a29a1636f 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.component.tsx @@ -1,12 +1,17 @@ import React, { useState } from 'react'; import i18n from '@dhis2/d2-i18n'; import { Modal, ModalContent, ModalTitle, ModalActions, ButtonStrip, Button, NoticeBox } from '@dhis2/ui'; +import { getTermLabelFromProgram, LabelKeys } from '../../../../../metaData'; import type { Props } from './DeleteModal.types'; import { useDeleteTrackedEntity } from './hooks'; import type { ErrorReport } from '../../processErrorReports'; -export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, setOpenModal, onDeleteSuccess }: Props) => { +export const DeleteModal = ({ trackedEntityTypeName, trackedEntity, program, setOpenModal, onDeleteSuccess }: Props) => { const [errorReports, setErrorReports] = useState>([]); + const { enrollmentsLabel, eventsLabel } = getTermLabelFromProgram( + [LabelKeys.enrollmentPlural, LabelKeys.eventPlural], + { program }, + ); const handleErrors = (errors: Array) => { setErrorReports(errors); }; @@ -23,10 +28,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 {{enrollmentsLabel}} and {{eventsLabel}} in all programs.', { trackedEntityTypeName, - interpolation: { escapeValue: false }, + enrollmentsLabel, + eventsLabel, }, )}

diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.types.ts b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.types.ts index 780a5b9fb6..4c4f5ff0d6 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/Delete/DeleteModal/DeleteModal.types.ts @@ -1,6 +1,7 @@ export type Props = { trackedEntity: { trackedEntity: string }; trackedEntityTypeName: string; + program: Record; setOpenModal: (toggle: boolean) => void; onDeleteSuccess?: () => void; }; diff --git a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.tsx index e6c443a798..9530672bee 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/OverflowMenu/OverflowMenu.component.tsx @@ -73,6 +73,7 @@ const Modals = ({ diff --git a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx index fbfcd015a9..d689b76deb 100644 --- a/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx +++ b/src/core_modules/capture-core/components/WidgetProfile/WidgetProfile.component.tsx @@ -11,6 +11,7 @@ import { Widget } from '../Widget'; import { LoadingMaskElementCenter } from '../LoadingMasks'; import { NoticeBox } from '../NoticeBox'; import type { Props } from './widgetProfile.types'; +import { getTermLabelFromProgram, LabelKeys } from '../../metaData'; import { useProgram, useTrackedEntityInstances, @@ -81,6 +82,7 @@ const WidgetProfilePlain = ({ const [open, setOpenStatus] = useState(true); const [modalState, setTeiModalState] = useState(TEI_MODAL_STATE.CLOSE); const { loading: programsLoading, program, error: programsError } = useProgram(programId); + const { attributeLabel } = getTermLabelFromProgram([LabelKeys.attributeSingular], { program }); const { storedAttributeValues, storedGeometry, hasError } = useSelector(({ trackedEntityInstance }: any) => ({ storedAttributeValues: trackedEntityInstance?.attributeValues, storedGeometry: trackedEntityInstance?.geometry, @@ -176,11 +178,12 @@ const WidgetProfilePlain = ({

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

); diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/EnterDataInOrgUnit/EnterData.component.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/EnterDataInOrgUnit/EnterData.component.tsx index ac4266cf14..96274ef7f9 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/EnterDataInOrgUnit/EnterData.component.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/EnterDataInOrgUnit/EnterData.component.tsx @@ -1,10 +1,11 @@ -import React, { type ComponentType } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { type ComponentType } from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { colors, spacers, spacersNum, IconInfo16 } from '@dhis2/ui'; import { OrgUnitSelectorForRelatedStages } from '../FormComponents'; import type { ErrorMessagesForRelatedStages } from '../RelatedStagesActions'; import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; +import { LabelKeys, useTermLabel } from '../../../metaData'; const styles: Readonly = { wrapper: { @@ -60,6 +61,7 @@ export const EnterDataInOrgUnitPlain = ({ errorMessages, classes, }: Props) => { + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); const onSelectOrgUnit = (e: { id: string; displayName: string; path: string }) => { const orgUnit = { id: e.id, @@ -95,11 +97,12 @@ export const EnterDataInOrgUnitPlain = ({ {i18n.t( relatedStagesDataValues?.orgUnit?.name - ? 'Enter {{linkableStageLabel}} details for {{orgUnitLabel}} in the next step' - : 'Select organisation unit and enter {{linkableStageLabel}} details in the next step', + ? 'Enter {{linkableStageLabel}} details for {{orgUnitName}} in the next step' + : 'Select {{orgUnitLabel}} and enter {{linkableStageLabel}} details in the next step', { linkableStageLabel, - orgUnitLabel: relatedStagesDataValues?.orgUnit?.name, + orgUnitName: relatedStagesDataValues?.orgUnit?.name, + orgUnitLabel, }, )}
diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx index 54e0332b6e..7cbc1d91e4 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/FormComponents/OrgUnitSelectorForRelatedStages.tsx @@ -11,6 +11,7 @@ import labelTypeClasses from './dataEntryFieldLabels.module.css'; import { baseInputStyles } from './commonProps'; import type { ErrorMessagesForRelatedStages } from '../RelatedStagesActions'; import type { RelatedStageDataValueStates } from '../WidgetRelatedStages.types'; +import { LabelKeys, useTermLabel } from '../../../metaData'; type OrgUnitValue = { checked: boolean; @@ -49,6 +50,7 @@ export const OrgUnitSelectorForRelatedStages = ({ saveAttempted, }: Props) => { const [touched, setTouched] = useState(false); + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); const handleSelect = (event: OrgUnitValue) => { setTouched(true); @@ -64,7 +66,7 @@ export const OrgUnitSelectorForRelatedStages = ({ return ( { const [touched, setTouched] = useState(false); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular]); const handleChange = (value: string | null) => { setTouched(true); @@ -47,8 +49,9 @@ export const LinkToExisting = ({ label: event.label, })); - const label = i18n.t('Choose a {{linkableStageLabel}} event', { + const label = i18n.t('Choose a {{linkableStageLabel}} {{eventLabel}}', { linkableStageLabel, + eventLabel, }); const shouldShowError = (saveAttempted || touched); @@ -61,7 +64,7 @@ export const LinkToExisting = ({ onChange={handleChange} onBlur={handleBlur} options={options} - placeholder={i18n.t('Select an event')} + placeholder={i18n.t('Select an {{eventLabel}}', { eventLabel })} clearable styles={baseInputStyles} errorMessage={shouldShowError ? errorMessages.linkedEventId : undefined} diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx index 3791e98e55..c6effeb9f4 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.component.tsx @@ -11,6 +11,7 @@ import { useProgramStageInfo } from '../../../metaDataMemoryStores/programCollec import type { PlainProps, LinkButtonProps } from './RelatedStagesActions.types'; import { LinkToExisting } from '../LinkToExisting'; import { EnterDataInOrgUnit } from '../EnterDataInOrgUnit/EnterData.component'; +import { LabelKeys, useTermLabel } from '../../../metaData'; const styles: Readonly = { wrapper: { @@ -49,6 +50,7 @@ const Schedule = ({ programStage, canAddNewEventToStage, }) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { stageId: programStage?.id }); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.SCHEDULE_IN_ORG] || {}; if (hidden) { @@ -60,9 +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 {{eventLabel}}', { linkableStageLabel: programStage.stageForm.name, - interpolation: { escapeValue: false }, + eventLabel, }); } @@ -93,6 +95,7 @@ const EnterData = ({ programStage, canAddNewEventToStage, }) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { stageId: programStage?.id }); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.ENTER_DATA] || {}; if (hidden) { @@ -104,9 +107,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 {{eventLabel}}', { linkableStageLabel: programStage.stageForm.name, - interpolation: { escapeValue: false }, + eventLabel, }); } @@ -137,6 +140,10 @@ const LinkExistingResponse = ({ updateSelectedAction, programStage, }) => { + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + { stageId: programStage?.id }, + ); const { hidden, disabled, disabledMessage } = actionsOptions?.[relatedStageActions.LINK_EXISTING_RESPONSE] || {}; if (hidden) { @@ -148,9 +155,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 {{eventsLabel}}', { linkableStageLabel: programStage.stageForm.name, - interpolation: { escapeValue: false }, + eventsLabel, }); } @@ -165,7 +172,7 @@ const LinkExistingResponse = ({ name={`related-stage-action-${relatedStageActions.LINK_EXISTING_RESPONSE}`} checked={relatedStageActions.LINK_EXISTING_RESPONSE === selectedAction} disabled={tooltipEnabled} - label={mainOptionTranslatedTexts[relatedStageActions.LINK_EXISTING_RESPONSE]} + label={i18n.t('Link to an existing {{eventLabel}}', { eventLabel })} onChange={e => updateSelectedAction(e.value)} value={relatedStageActions.LINK_EXISTING_RESPONSE} dataTest="related-stages-actions-link-existing-response" @@ -207,6 +214,7 @@ const RelatedStagesActionsPlain = ({ isLinking, }: PlainProps & WithStyles) => { const { programStage } = useProgramStageInfo(constraint?.programStage?.id); + const { relationshipsLabel } = useTermLabel([LabelKeys.relationshipPlural]); const selectedAction = useMemo(() => relatedStagesDataValues.linkMode, [relatedStagesDataValues.linkMode]); @@ -255,7 +263,9 @@ const RelatedStagesActionsPlain = ({ )} {type === relatedStageStatus.AMBIGUOUS_RELATIONSHIPS && ( -
{i18n.t('Ambiguous relationships, contact system administrator')}
+
+ {i18n.t('Ambiguous {{relationshipsLabel}}, contact system administrator', { relationshipsLabel })} +
)}
diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx index ffe0be161b..4b341eb12f 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/RelatedStagesActions/RelatedStagesActions.container.tsx @@ -7,6 +7,7 @@ import { relatedStageStatus, relatedStageActions } from '../constants'; import { useStageLabels, useRelatedStageEvents, useRelatedStages } from '../hooks'; import { relatedStageWidgetIsValid } from '../relatedStageEventIsValid/relatedStageEventIsValid'; import { useProgramExpiryForUser } from '../../../hooks'; +import { LabelKeys, useTermLabel } from '../../../metaData'; type RefHandle = { eventHasLinkableStageRelationship: () => boolean; @@ -50,6 +51,8 @@ const RelatedStagesActionsPlain = ({ }); const { isLoading: orgUnitLoading, data } = useOrgUnitAutoSelect(); const expiryPeriod = useProgramExpiryForUser(programId); + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular], { programId }); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId, stageId: constraint?.programStage?.id }); useEffect(() => { if (!orgUnitLoading && (data as any)?.length === 1) { @@ -84,8 +87,10 @@ const RelatedStagesActionsPlain = ({ linkedEventId, expiryPeriod, setErrorMessages: addErrorMessage, + orgUnitLabel, + eventLabel, }); - }, [relatedStageDataValues, expiryPeriod]); + }, [relatedStageDataValues, expiryPeriod, orgUnitLabel, eventLabel]); const getLinkedStageValues = () => ({ linkMode: relatedStageDataValues.linkMode, 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..0edd461e99 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/WidgetRelatedStages.container.tsx @@ -1,7 +1,7 @@ +import i18n from '@dhis2/d2-i18n'; import React, { useRef, useCallback, useState } from 'react'; import { IconLink24, spacers } from '@dhis2/ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import i18n from '@dhis2/d2-i18n'; import { Widget } from '../Widget'; import { RelatedStagesActions } from './RelatedStagesActions'; import { useLinkedEventByOriginId } from '../WidgetTwoEventWorkspace/hooks'; @@ -16,6 +16,7 @@ import { relatedStageStatus } from './constants'; import { useCommonEnrollmentDomainData } from '../Pages/common/EnrollmentOverviewDomain'; import { useEnrollmentAccessContext } from '../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; import type { RequestEvent } from '../DataEntries'; +import { LabelKeys, useTermLabel } from '../../metaData'; const styles = { header: { @@ -48,6 +49,7 @@ export const WidgetRelatedStagesPlain = ({ classes, }: Props) => { const [isLinking, setIsLinking] = useState(false); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId, stageId: programStageId }); const { enrollment } = useCommonEnrollmentDomainData(teiId, enrollmentId, programId); const { currentRelatedStagesStatus, constraint } = useRelatedStages({ programStageId, programId }); const { stageWriteAccessById } = useEnrollmentAccessContext(); @@ -123,7 +125,7 @@ export const WidgetRelatedStagesPlain = ({ - {i18n.t('Linked event')} + {i18n.t('Linked {{eventLabel}}', { eventLabel })}
} > diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts index 08d2aa16e2..74c218780b 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/constants.ts @@ -9,7 +9,6 @@ export const relatedStageActions = Object.freeze({ export const mainOptionTranslatedTexts = { [relatedStageActions.SCHEDULE_IN_ORG]: i18n.t('Schedule'), [relatedStageActions.ENTER_DATA]: i18n.t('Enter details now'), - [relatedStageActions.LINK_EXISTING_RESPONSE]: i18n.t('Link to an existing event'), }; export const relatedStageStatus = Object.freeze({ diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/hooks/useAddEventWithRelationship.ts index 4dbc628392..ade05c775b 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 { LabelKeys, useTermLabel } from '../../../metaData'; const ReactQueryAppNamespace = 'capture'; @@ -30,6 +31,7 @@ export const useAddEventWithRelationship = ({ const queryClient = useQueryClient(); const { show: showSuccess } = useAlert(({ message }) => message, { success: true }); const { show: showAlert } = useAlert(({ message }) => message, { critical: true }); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular]); const { mutate } = useMutation( ({ serverData }: { serverData: any }) => @@ -55,12 +57,22 @@ 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 {{eventLabel}} was successfully linked', + { eventLabel }, + ), + }); } }, 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 {{eventLabel}}', + { eventLabel }, + ), + }); onUpdateEnrollmentEventsError && onUpdateEnrollmentEventsError((payload.serverData as any).events); }, }, diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts index b4f0a63c79..739a2395c0 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/ValidationFunctions.ts @@ -15,6 +15,8 @@ type Props = { expiryPeriodType?: string; expiryDays?: number; }; + orgUnitLabel: string; + eventLabel: string; }; export const isScheduledDateValid = ( @@ -62,6 +64,7 @@ const scheduleInOrgUnit = (props) => { orgUnit, setErrorMessages, expiryPeriod, + orgUnitLabel, } = props ?? {}; const { valid: scheduledAtIsValid, validationText } = isScheduledDateValid( scheduledAt, @@ -82,7 +85,7 @@ const scheduleInOrgUnit = (props) => { if (!orgUnitIsValid) { setErrorMessages({ - orgUnit: i18n.t('Please provide a valid organisation unit'), + orgUnit: i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), }); } else { setErrorMessages({ @@ -94,12 +97,12 @@ const scheduleInOrgUnit = (props) => { }; const enterData = (props) => { - const { orgUnit, setErrorMessages } = props ?? {}; + const { orgUnit, setErrorMessages, orgUnitLabel } = props ?? {}; const orgUnitIsValid = isValidOrgUnit(orgUnit); if (!orgUnitIsValid) { setErrorMessages({ - orgUnit: i18n.t('Please provide a valid organisation unit'), + orgUnit: i18n.t('Please provide a valid {{orgUnitLabel}}', { orgUnitLabel }), }); } else { setErrorMessages({ @@ -111,12 +114,12 @@ const enterData = (props) => { }; const linkToExistingResponse = (props) => { - const { linkedEventId, setErrorMessages } = props ?? {}; + const { linkedEventId, setErrorMessages, eventLabel } = props ?? {}; const linkedEventIdIsValid = !!linkedEventId; if (!linkedEventIdIsValid) { setErrorMessages({ - linkedEventId: i18n.t('Please select a valid event'), + linkedEventId: i18n.t('Please select a valid {{eventLabel}}', { eventLabel }), }); } else { setErrorMessages({ diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts index f8e5943702..5ba09fe2f6 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.ts @@ -12,6 +12,8 @@ export const relatedStageWidgetIsValid = ({ linkedEventId, setErrorMessages, expiryPeriod, + orgUnitLabel, + eventLabel, }: RelatedStageIsValidProps) => { if (!linkMode) { return true; @@ -31,5 +33,7 @@ export const relatedStageWidgetIsValid = ({ linkedEventId, setErrorMessages, expiryPeriod, + orgUnitLabel, + eventLabel, }); }; diff --git a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts index c6d401ef37..c97368188e 100644 --- a/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts +++ b/src/core_modules/capture-core/components/WidgetRelatedStages/relatedStageEventIsValid/relatedStageEventIsValid.types.ts @@ -16,4 +16,6 @@ export type RelatedStageIsValidProps = { expiryPeriodType?: string; expiryDays?: number; }; + orgUnitLabel: string; + eventLabel: string; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx index 0950bee130..722576efe4 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/Stage.component.tsx @@ -53,6 +53,8 @@ export const StagePlain = ({ description={description} events={events} stageWriteAccess={effectiveStageWriteAccess} + programId={passOnProps.programId} + stageId={id} />} onOpen={handleOpen} onClose={handleClose} @@ -77,6 +79,7 @@ export const StagePlain = ({ repeatable={repeatable} preventAddingEventActionInEffect={preventAddingNewEvents} eventName={name} + stageId={id} />
)} 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..fcc7c6b4c5 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageCreateNewButton/StageCreateNewButton.tsx @@ -1,7 +1,8 @@ +import i18n from '@dhis2/d2-i18n'; import React, { useMemo } from 'react'; import { Button, IconAdd16 } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../../../../Tooltips/ConditionalTooltip'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; type Props = { onCreateNew: () => void; @@ -9,6 +10,7 @@ type Props = { repeatable?: boolean; preventAddingEventActionInEffect?: boolean; eventName: string; + stageId: string; }; export const StageCreateNewButton = ({ @@ -17,28 +19,36 @@ export const StageCreateNewButton = ({ repeatable, preventAddingEventActionInEffect, eventName, + stageId, }: Props) => { + const { programStageLabel, eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.programStageSingular, LabelKeys.eventSingular, LabelKeys.eventPlural], + { stageId }, + ); 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 }} {{eventsLabel}}", { programStageName: eventName, - interpolation: { escapeValue: false }, + eventsLabel, }), }; } if (!repeatable && eventCount > 0) { return { isDisabled: true, - tooltipContent: i18n.t('This stage can only have one event'), + tooltipContent: i18n.t( + 'This {{programStageLabel}} can only have one {{eventLabel}}', + { programStageLabel, eventLabel }, + ), }; } return { isDisabled: false, tooltipContent: '', }; - }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable]); + }, [eventCount, eventName, preventAddingEventActionInEffect, repeatable, programStageLabel, eventLabel, eventsLabel]); return ( - {i18n.t('New {{ eventName }} event', { - eventName, interpolation: { escapeValue: false }, + {i18n.t('New {{ eventName }} {{eventLabel}}', { + eventName, eventLabel, })} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx index cd1ee4f139..05016aea02 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionButton/DeleteActionButton.tsx @@ -7,7 +7,7 @@ import { } from '@dhis2/ui'; import { ConditionalTooltip } from '../../../../../../Tooltips/ConditionalTooltip'; import { convertClientToView, convertServerToClient } from '../../../../../../../converters'; -import { dataElementTypes, type ProgramStage } from '../../../../../../../metaData'; +import { dataElementTypes, LabelKeys, type ProgramStage, useTermLabel } from '../../../../../../../metaData'; import { useEventEditPermissions } from '../../../../../../../hooks'; type Props = { @@ -31,6 +31,7 @@ export const DeleteActionButton = ({ }: Props) => { const occurredAtClient = convertServerToClient(occurredAt, dataElementTypes.DATE) as string; const occurredAtClientView = convertClientToView(occurredAtClient, dataElementTypes.DATE); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId, stageId: programStage?.id }); const { isEventWithinValidPeriod, @@ -46,15 +47,15 @@ export const DeleteActionButton = ({ const getDisabledMessage = (): string => { if (!isEventWithinValidPeriod) { - return i18n.t('{{occurredAt}} belongs to an expired period. Event cannot be deleted', { + return i18n.t('{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be deleted', { occurredAt: occurredAtClientView, - interpolation: { escapeValue: false }, + eventLabel, }); } if (!canEditCompletedEvent) { - return i18n.t('This event has been completed'); + return i18n.t('This {{eventLabel}} has been completed', { eventLabel }); } - return i18n.t('This event is outside the edit period'); + return i18n.t('This {{eventLabel}} is outside the edit period', { eventLabel }); }; return ( diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx index 2883536f0f..6ffbad778d 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/DeleteActionModal/DeleteActionModal.tsx @@ -6,6 +6,7 @@ import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useMutation } from '@tanstack/react-query'; import { errorCreator } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; type Props = { eventId: string; @@ -24,6 +25,10 @@ export const DeleteActionModal = ({ onDeleteEvent, onRollbackDeleteEvent, }: Props) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { + programId: eventDetails.program, + stageId: eventDetails.programStage, + }); const { show: showError } = useAlert( ({ message }) => message, { @@ -54,7 +59,12 @@ export const DeleteActionModal = ({ return eventToRollbackOnFail; }, onError: (apiError: unknown, payload: unknown, eventToRollbackOnFail?: ApiEnrollmentEvent) => { - showError({ message: i18n.t('An error occurred while deleting the event') }); + showError({ + message: i18n.t( + 'An error occurred while deleting the {{eventLabel}}', + { eventLabel }, + ), + }); log.error(errorCreator('An error occurred while deleting the event')({ apiError, payload })); if (eventToRollbackOnFail) { @@ -70,13 +80,13 @@ export const DeleteActionModal = ({ small > - {i18n.t('Delete event')} + {i18n.t('Delete {{eventLabel}}', { eventLabel })}

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

@@ -90,7 +100,7 @@ export const DeleteActionModal = ({ destructive onClick={() => !pendingApiResponse && mutate({ eventId })} > - {i18n.t('Yes, delete event')} + {i18n.t('Yes, delete {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx index 322c291ec1..086c051081 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/EventRow/SkipAction/SkipAction.tsx @@ -11,6 +11,7 @@ import { errorCreator } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; import { DirectionalArrow } from '../../../../../../../utils/rtl'; import { EventStatuses } from '../EventRow'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; type Props = { eventId: string; @@ -28,6 +29,10 @@ export const SkipAction = ({ onUpdateEventStatus, }: Props) => { const dataEngine = useDataEngine(); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { + programId: eventDetails.program, + stageId: eventDetails.programStage, + }); const { show: showError } = useAlert( ({ message }) => message, { critical: true }, @@ -56,7 +61,12 @@ 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 {{eventLabel}} status', + { eventLabel }, + ), + }); log.error(errorCreator('An error occurred when updating event status')({ error, payload, context })); context && onUpdateEventStatus(eventId, context.previousStatus); }, diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/StageDetail.component.tsx index ada5786b24..0e6067ff71 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 { LabelKeys, useTermLabel } from '../../../../../metaData'; import type { Props } from './stageDetail.types'; import { EventRow } from './EventRow'; import { useClientDataElements } from './hooks/useClientDataElements'; @@ -107,9 +108,15 @@ const StageDetailPlain = (props: Props & WithStyles) => { sortDirection: SORT_DIRECTION.DESC, }; const { stage } = getProgramAndStageForProgram(programId, stageId); + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + { programId, stageId }, + ); const { stageWriteAccessById } = useEnrollmentAccessContext(); const stageWriteAccess = stageWriteAccessById[stageId] ?? stage?.access?.data?.write; - const headerColumns = useComputeHeaderColumn(dataElements, hideDueDate, enableUserAssignment, stage?.stageForm); + const headerColumns = useComputeHeaderColumn( + dataElements, hideDueDate, enableUserAssignment, stage?.stageForm, programId, stageId, + ); const dataElementsClient = useClientDataElements(dataElements); const { loading, value: dataSource, error } = useComputeDataFromEvent(dataElementsClient, events); @@ -180,7 +187,10 @@ const StageDetailPlain = (props: Props & WithStyles) => { const cells = headerColumns.map(({ id }) => ( {({ onMouseOver, onMouseOut, ref }) => ( @@ -264,6 +274,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { preventAddingEventActionInEffect={hiddenProgramStage} repeatable={repeatable} eventName={eventName} + stageId={stageId} />
) : null); @@ -281,7 +292,7 @@ const StageDetailPlain = (props: Props & WithStyles) => { if (error) { return (
- {i18n.t('Events could not be retrieved. Please try again later.')} + {i18n.t('{{eventsLabel}} could not be retrieved. Please try again later.', { eventsLabel })}
); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts index 668a6e316f..c8abba4ccd 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageDetail/hooks/useEventList.ts @@ -5,7 +5,7 @@ import { useDataEngine, useConfig } from '@dhis2/app-runtime'; import { makeQuerySingleResource } from 'capture-core/utils/api'; import { errorCreator, buildUrl } from 'capture-core-utils'; import type { ApiEnrollmentEvent } from 'capture-core-utils/types/api-types'; -import { dataElementTypes, DataElement, OptionSet, Option } from '../../../../../../metaData'; +import { dataElementTypes, DataElement, LabelKeys, OptionSet, Option, useTermLabel } from '../../../../../../metaData'; import type { StageDataElement, StageDataElementClient } from '../../../../types/common.types'; import { convertValue as convertClientToList } from '../../../../../../converters/clientToList'; import { convertValue as convertServerToClient } from '../../../../../../converters/serverToClient'; @@ -37,7 +37,7 @@ const getBaseColumnHeaders = props => [ { header: i18n.t('Status'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: props.formFoundation.getLabel('occurredAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: i18n.t('Assigned to'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, - { header: i18n.t('Organisation unit'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, + { header: props.orgUnitLabel, sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: props.formFoundation.getLabel('scheduledAt'), sortDirection: SORT_DIRECTION.DEFAULT, isPredefined: true }, { header: '', sortDirection: null, isPredefined: true }, ]; @@ -117,7 +117,11 @@ const useComputeHeaderColumn = ( hideDueDate: boolean, enableUserAssignment: boolean, formFoundation?: { getLabel: (key: string) => string }, + programId?: string, + stageId?: string, ) => { + const { orgUnitLabel: rawOrgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular], { programId, stageId }); + const orgUnitLabel = i18n.t('{{orgUnitLabel}}', { orgUnitLabel: rawOrgUnitLabel }); const headerColumns = useMemo(() => { const dataElementHeaders = dataElements.reduce((acc, currDataElement) => { const { id, name, formName, type, optionSet } = currDataElement; @@ -131,13 +135,13 @@ const useComputeHeaderColumn = ( return acc; }, [] as Array<{ id: string; header: string; type: keyof typeof dataElementTypes; sortDirection: string }>); return [ - ...getBaseColumns({ formFoundation }) + ...getBaseColumns({ formFoundation, orgUnitLabel }) .filter(col => (enableUserAssignment || col.id !== 'assignedUser') && (!hideDueDate || col.id !== 'scheduledAt'), ), ...dataElementHeaders]; - }, [dataElements, hideDueDate, enableUserAssignment, formFoundation]); + }, [dataElements, hideDueDate, enableUserAssignment, formFoundation, orgUnitLabel]); return headerColumns; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/StageOverview.component.tsx index d87ef71abc..e0abec5418 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, LabelKeys, useTermLabel } from '../../../../../metaData'; const styles: Readonly = { container: { @@ -93,7 +93,7 @@ const getLastUpdatedAt = (events: Array, fromServerDate: (da }; export const StageOverviewPlain = ({ - title, icon, description, events, stageWriteAccess = true, classes, + title, icon, description, events, stageWriteAccess = true, programId, stageId, classes, }: Props & WithStyles) => { const { fromServerDate } = useTimeZoneConversion(); const { anyStageWriteAccess, showWidgetBadge } = useEnrollmentAccessContext(); @@ -101,6 +101,10 @@ export const StageOverviewPlain = ({ const totalEvents = events.length; const overdueEvents = events.filter(isEventOverdue).length; const scheduledEvents = events.filter(event => event.status === statusTypes.SCHEDULE).length; + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + { programId, stageId }, + ); return (
@@ -136,10 +140,12 @@ export const StageOverviewPlain = ({
- {i18n.t('{{ count }} event', { + {i18n.t('{{count}} {{eventLabel}}', { count: totalEvents, - defaultValue: '{{ count }} event', - defaultValue_plural: '{{count}} events', + eventLabel, + eventsLabel, + defaultValue: '{{count}} {{eventLabel}}', + defaultValue_plural: '{{count}} {{eventsLabel}}', })}
{overdueEvents > 0 ?
@@ -163,6 +169,8 @@ export const StageOverviewPlain = ({ {showStageBadge && ( )}
diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts index 8889520a25..6bca6f9d61 100644 --- a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts +++ b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stage/StageOverview/stageOverview.types.ts @@ -7,4 +7,6 @@ export type Props = { icon?: Icon; description?: string | null; stageWriteAccess?: boolean; + programId?: string; + stageId: string; }; diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/Stages/Stages.component.tsx index 6ee363c119..2729a40b4e 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 @@ -1,11 +1,12 @@ -import React, { type ComponentType, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { type ComponentType, useMemo } from 'react'; import { colors, spacersNum } from '@dhis2/ui'; import { compose } from 'redux'; import { Stage } from './Stage'; import type { PlainProps, InputProps } from './stages.types'; import { withLoadingIndicator } from '../../../HOC'; import { useEnrollmentAccessContext } from '../../Pages/common/EnrollmentOverviewDomain/EnrollmentAccessContext'; +import { LabelKeys, useTermLabel } from '../../../metaData'; const emptyStateStyle = { padding: `0 ${spacersNum.dp12}px`, @@ -23,6 +24,10 @@ export const StagesPlain = ({ ...passOnProps }: PlainProps) => { const { stageReadAccessById } = useEnrollmentAccessContext(); + const { programStagesLabel } = useTermLabel( + [LabelKeys.programStagePlural], + { programId: passOnProps.programId }, + ); const readableStages = useMemo( () => stages.filter(stage => stageReadAccessById[stage.id] ?? stage.dataAccess.read), [stages, stageReadAccessById], @@ -52,7 +57,7 @@ export const StagesPlain = ({ if (!readableStages.length) { return (

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

); } diff --git a/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx b/src/core_modules/capture-core/components/WidgetStagesAndEvents/WidgetStagesAndEvents.component.tsx index 6a410d71ff..d03f72657a 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 { useTermLabel, LabelKeys } from '../../metaData'; import type { Props } from './stagesAndEvents.types'; const styles = { @@ -35,6 +36,11 @@ const WidgetStagesAndEventsPlain = ({ multipleStages, showWidgetBadge, } = useEnrollmentAccessContext(); + // Example use of useTermLabel. + const { programStagesLabel, eventsLabel } = useTermLabel( + [LabelKeys.programStagePlural, LabelKeys.eventPlural], + { programId }, + ); return (
- {i18n.t('Stages and Events')} + + {i18n.t('{{programStagesLabel}} and {{eventsLabel}}', { + programStagesLabel, + eventsLabel, + })} + {showWidgetBadge && (
)} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx index 0a3ce4e9f8..acc3490386 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.tsx @@ -13,6 +13,7 @@ import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers'; import type { Props } from './UnlinkAndDeleteModal.types'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; export const UnlinkAndDeleteModal = ({ setOpenModal, @@ -21,11 +22,13 @@ export const UnlinkAndDeleteModal = ({ relationshipId, onDeleteEvent, onDeleteEventRelationship, + stageId, }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { stageId }); 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 {{eventLabel}}.', { eventLabel }), { critical: true }, ); @@ -61,12 +64,18 @@ export const UnlinkAndDeleteModal = ({ return ( - {i18n.t('Unlink and delete linked event')} + {i18n.t('Unlink and delete linked {{eventLabel}}', { eventLabel })}

- {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 {{eventLabel}}?', + { eventLabel }, + )} {' '} - {i18n.t('This action permanently removes the link, linked event, and all related data.')} + {i18n.t( + 'This action permanently removes the link, linked {{eventLabel}}, and all related data.', + { eventLabel }, + )}

@@ -82,7 +91,7 @@ export const UnlinkAndDeleteModal = ({ onClick={() => mutation.mutate()} disabled={mutation.isLoading} > - {i18n.t('Yes, unlink and delete linked event')} + {i18n.t('Yes, unlink and delete linked {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts index 6e324de160..33d72f5998 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkAndDeleteModal.types.ts @@ -5,4 +5,5 @@ export type Props = { relationshipId: string; onDeleteEvent?: (eventId: string) => void; onDeleteEventRelationship?: (relationshipId: string) => void; + stageId?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx index 9c96cea626..5e7d863fa5 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.tsx @@ -13,17 +13,23 @@ import { useDataEngine, useAlert } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactQueryAppNamespace } from 'capture-core/utils/reactQueryHelpers'; import type { Props } from './UnlinkModal.types'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; export const UnlinkModal = ({ setOpenModal, relationshipId, originEventId, onDeleteEventRelationship, + stageId, }: Props) => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + { stageId }, + ); 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 {{eventLabel}}.', { eventLabel }), { critical: true }, ); @@ -59,13 +65,19 @@ export const UnlinkModal = ({ return ( - {i18n.t('Unlink event')} + {i18n.t('Unlink {{eventLabel}}', { eventLabel })}

- {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 {{eventsLabel}}?', + { eventsLabel }, + )} {' '} - {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 {{eventLabel}} will remain.', + { eventLabel }, + )}

@@ -79,7 +91,7 @@ export const UnlinkModal = ({ disabled={mutation.isLoading} dataTest="event-overflow-unlink-event-confirm" > - {i18n.t('Yes, unlink event')} + {i18n.t('Yes, unlink {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts index 144dde66c9..2b97ac46e4 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/Modal/UnlinkModal.types.ts @@ -4,4 +4,5 @@ export type Props = { originEventId: string; onDeleteEvent?: (eventId: string) => void; onDeleteEventRelationship?: (relationshipId: string) => void; + stageId?: string; }; diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/OverflowMenu/OverflowMenu.component.tsx index 11f07eb94b..4b161e5df6 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 @@ -1,3 +1,4 @@ +import i18n from '@dhis2/d2-i18n'; import React, { useState } from 'react'; import { Divider, @@ -8,13 +9,13 @@ import { IconView16, MenuItem, } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { ConditionalTooltip } from '../../Tooltips/ConditionalTooltip'; import { OverflowButton } from '../../Buttons'; import { UnlinkModal, UnlinkAndDeleteModal } from './Modal'; import { useNavigate, buildUrlQueryString } from '../../../utils/routing'; import type { Props } from './OverflowMenu.types'; import { useRelationshipTypeAccess } from '../hooks'; +import { LabelKeys, useTermLabel } from '../../../metaData'; export const OverflowMenuComponent = ({ linkedEvent, @@ -31,6 +32,10 @@ export const OverflowMenuComponent = ({ const [isUnlinkModalOpen, setIsUnlinkModalOpen] = useState(false); const [isUnlinkAndDeleteModalOpen, setIsUnlinkAndDeleteModalOpen] = useState(false); const { relationshipTypeWriteAccess } = useRelationshipTypeAccess(relationshipType); + const { eventLabel, eventsLabel } = useTermLabel( + [LabelKeys.eventSingular, LabelKeys.eventPlural], + { stageId: linkedEvent?.programStage }, + ); const handleViewLinkedEvent = () => { navigate(`/enrollmentEventEdit?${buildUrlQueryString({ eventId: linkedEvent.event, orgUnitId })}`); @@ -59,7 +64,7 @@ export const OverflowMenuComponent = ({ component={ } dataTest="event-overflow-view-linked-event" onClick={handleViewLinkedEvent} @@ -67,11 +72,14 @@ export const OverflowMenuComponent = ({ /> } disabled={!stageWriteAccess || !relationshipTypeWriteAccess} dense @@ -81,11 +89,14 @@ export const OverflowMenuComponent = ({ /> } disabled={!stageWriteAccess || !relationshipTypeWriteAccess} dense @@ -104,6 +115,7 @@ export const OverflowMenuComponent = ({ relationshipId={relationshipId} originEventId={originEventId} onDeleteEventRelationship={onDeleteEventRelationship} + stageId={linkedEvent?.programStage} /> )} {isUnlinkAndDeleteModalOpen && ( @@ -114,6 +126,7 @@ export const OverflowMenuComponent = ({ relationshipId={relationshipId} onDeleteEvent={onDeleteEvent} onDeleteEventRelationship={onDeleteEventRelationship} + stageId={linkedEvent?.programStage} /> )} diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx index 7992e0bcfb..645b0369f0 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetTwoEventWorkspace.component.tsx @@ -1,8 +1,10 @@ import React, { useMemo } from 'react'; +import i18n from '@dhis2/d2-i18n'; import { spacers } from '@dhis2/ui'; import { FlatList } from 'capture-ui'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { RenderFoundation } from '../../metaData'; +import { LabelKeys, useTermLabel } from '../../metaData'; import { getDataEntryDetails, Placements } from './utils/getDataEntryDetails'; type OwnProps = { @@ -23,10 +25,16 @@ const styles: Readonly = { }; const WidgetTwoEventWorkspacePlain = ({ linkedEvent, dataValues, formFoundation, classes }: Props) => { + const { orgUnitLabel: rawOrgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular], { + programId: linkedEvent?.program, + stageId: linkedEvent?.programStage, + }); + const orgUnitLabel = i18n.t('{{orgUnitLabel}}', { orgUnitLabel: rawOrgUnitLabel }); const dataEntryValues = useMemo(() => getDataEntryDetails( linkedEvent, formFoundation, - ), [linkedEvent, formFoundation]); + orgUnitLabel, + ), [linkedEvent, formFoundation, orgUnitLabel]); const listValues = useMemo(() => { const elements = formFoundation.getElements(); diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx index 4f08e0af71..66b82e8247 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/WidgetWrapper/WidgetWrapper.container.tsx @@ -1,9 +1,10 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { colors, spacersNum, IconLink16 } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import type { PlainProps } from './WidgetWrapper.types'; import { WidgetTwoEventWorkspaceWrapperTypes } from '../index'; +import { LabelKeys, useTermLabel } 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 { eventLabel } = useTermLabel([LabelKeys.eventSingular], { stageId: stage?.id }); if (type === WidgetTwoEventWorkspaceWrapperTypes.EDIT_EVENT) { return (
@@ -49,16 +51,17 @@ const WidgetWrapperPlain = ({ widget, type, stage, linkedStage, classes }: Plain -
{i18n.t('Linked event')}
+
{i18n.t('Linked {{eventLabel}}', { eventLabel })}
{linkedStage?.name && stage?.name ? - // eslint-disable-next-line max-len - i18n.t('This {{stageName}} event is linked to a {{linkedStageName}} event. Review the linked event details before entering data below', + i18n.t( + 'This {{stageName}} {{eventLabel}} is linked to a {{linkedStageName}} {{eventLabel}}. ' + + 'Review the linked {{eventLabel}} details before entering data below', { linkedStageName: linkedStage.name, stageName: stage.name, - interpolation: { escapeValue: false }, + eventLabel, }, ) : ''}
diff --git a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts index 892b22a4a0..b891ec52db 100644 --- a/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts +++ b/src/core_modules/capture-core/components/WidgetTwoEventWorkspace/utils/getDataEntryDetails.ts @@ -13,42 +13,42 @@ export const Placements = { BOTTOM: 'BOTTOM', }; -const StatusLabels = { - ACTIVE: i18n.t('Active'), - COMPLETED: i18n.t('Completed'), - CANCELLED: i18n.t('Cancelled'), - SCHEDULE: i18n.t('Scheduled'), -}; +export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: RenderFoundation, orgUnitLabel: string) => { + const statusLabels: Record = { + ACTIVE: i18n.t('Active'), + COMPLETED: i18n.t('Completed'), + CANCELLED: i18n.t('Cancelled'), + SCHEDULE: i18n.t('Scheduled'), + }; -const DataEntryFieldsToInclude = { - occurredAt: { - apiKey: 'occurredAt', - type: dataElementTypes.DATE, - placement: Placements.TOP, - }, - scheduledAt: { - apiKey: 'scheduledAt', - type: dataElementTypes.DATE, - placement: Placements.TOP, - }, - orgUnit: { - apiKey: 'orgUnit', - type: dataElementTypes.ORGANISATION_UNIT, - placement: Placements.TOP, - label: i18n.t('Organisation unit'), - convertFn: (orgUnitId: string) => React.createElement(TooltipOrgUnit, { orgUnitId }), - }, - status: { - apiKey: 'status', - type: dataElementTypes.TEXT, - placement: Placements.BOTTOM, - label: i18n.t('Status'), - convertFn: (value: keyof typeof StatusLabels) => StatusLabels[value], - }, -}; + const dataEntryFieldsToInclude = { + occurredAt: { + apiKey: 'occurredAt', + type: dataElementTypes.DATE, + placement: Placements.TOP, + }, + scheduledAt: { + apiKey: 'scheduledAt', + type: dataElementTypes.DATE, + placement: Placements.TOP, + }, + orgUnit: { + apiKey: 'orgUnit', + type: dataElementTypes.ORGANISATION_UNIT, + placement: Placements.TOP, + label: orgUnitLabel, + convertFn: (orgUnitId: string) => React.createElement(TooltipOrgUnit, { orgUnitId }), + }, + status: { + apiKey: 'status', + type: dataElementTypes.TEXT, + placement: Placements.BOTTOM, + label: i18n.t('Status'), + convertFn: (value: string) => statusLabels[value], + }, + }; -export const getDataEntryDetails = (linkedEvent: LinkedEvent, formFoundation: RenderFoundation) => { - const dataEntryValues = Object.values(DataEntryFieldsToInclude).map((entry: any) => { + const dataEntryValues = Object.values(dataEntryFieldsToInclude).map((entry: any) => { const value = linkedEvent[entry.apiKey]; if (!value) return null; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/Breadcrumbs.component.tsx index 9d34dc9ed9..9902233935 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 @@ -42,9 +42,10 @@ const Breadcrumblink = withStyles(breadcrumblinkStyles)( const Slash = withStyles(slashStyles)(({ classes }: WithStyles) => /); -const LinkedEntityMetadataSelectorStep = ({ currentStep, onNavigate, trackedEntityTypeName }: any) => { - const initialText = i18n.t('New {{trackedEntityTypeName}} relationship', { +const LinkedEntityMetadataSelectorStep = ({ currentStep, onNavigate, trackedEntityTypeName, relationshipLabel }: any) => { + const initialText = i18n.t('New {{trackedEntityTypeName}} {{relationshipLabel}}', { trackedEntityTypeName: trackedEntityTypeName && trackedEntityTypeName.toLowerCase(), + relationshipLabel, }); return (currentStep.value > NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA.value ? {initialText} : @@ -94,12 +95,14 @@ const BreadcrumbsPlain = ({ onNavigate, linkedEntityMetadataName, trackedEntityTypeName, + relationshipLabel, classes, }: Props) => (
onNavigate(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA)} /> diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.ts index d368b02cdb..343f481ce8 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/Breadcrumbs/breadcrumbs.types.ts @@ -9,4 +9,5 @@ export type PlainProps = { ) => void; readonly trackedEntityTypeName?: string; readonly linkedEntityMetadataName?: string; + readonly relationshipLabel: string; }; 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..77a3ad7651 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/NewTrackedEntityRelationship.component.tsx @@ -3,6 +3,7 @@ import i18n from '@dhis2/d2-i18n'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; import { Widget } from '../../../Widget'; import { LinkButton } from '../../../Buttons/LinkButton.component'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; import { Breadcrumbs } from './Breadcrumbs'; import { NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS, type WizardStep } from './wizardSteps.const'; import { @@ -50,6 +51,7 @@ const NewTrackedEntityRelationshipPlain = ({ onSelectFindMode, classes, }: ComponentProps & WithStyles) => { + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular], { programId }); const [currentStep, setCurrentStep] = useState(NEW_TRACKED_ENTITY_RELATIONSHIP_WIZARD_STEPS.SELECT_LINKED_ENTITY_METADATA); const [selectedLinkedEntityMetadata, setSelectedLinkedEntityMetadata] = @@ -275,7 +277,7 @@ const NewTrackedEntityRelationshipPlain = ({
- {i18n.t('Go back without saving relationship')} + {i18n.t('Go back without saving {{relationshipLabel}}', { relationshipLabel })}
)} > 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 c52f7f8df0..083c52633e 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 @@ -1,9 +1,10 @@ +import i18n from '@dhis2/d2-i18n'; 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 { NewTrackedEntityRelationshipPortal } from './NewTrackedEntityRelationship.portal'; import type { ContainerProps } from './NewTrackedEntityRelationship.types'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; const styles = { container: { @@ -29,6 +30,7 @@ const NewTrackedEntityRelationshipPlain = ({ classes, }: ContainerProps & WithStyles) => { const [addWizardVisible, setAddWizardVisible] = useState(false); + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular], { programId }); const closeAddWizard = useCallback(() => { setAddWizardVisible(false); @@ -48,7 +50,7 @@ const NewTrackedEntityRelationshipPlain = ({ small secondary > - {i18n.t('New Relationship')} + {i18n.t('New {{relationshipLabel}}', { relationshipLabel })} )} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.ts b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/NewTrackedEntityRelationship/hooks/useAddRelationship.ts index bdf38e55de..9618f61a4a 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 @@ -3,6 +3,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 { LabelKeys, useTermLabel } from '../../../../../metaData'; type Props = { teiId: string; @@ -21,8 +22,9 @@ const addRelationshipMutation: Mutation = { export const useAddRelationship = ({ teiId, onMutate, onSuccess }: Props) => { const queryClient = useQueryClient(); const dataEngine = useDataEngine(); + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular]); const { show: showAlert } = useAlert( - i18n.t('An error occurred while adding the relationship'), + i18n.t('An error occurred while adding the {{relationshipLabel}}', { relationshipLabel }), { critical: true }, ); diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx index 34b9048387..e5ff658312 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/WidgetTrackedEntityRelationship/WidgetTrackedEntityRelationship.component.tsx @@ -1,11 +1,12 @@ -import React, { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { useMemo } from 'react'; import type { WidgetTrackedEntityRelationshipProps } from './WidgetTrackedEntityRelationship.types'; import { RelationshipsWidget } from '../common/RelationshipsWidget'; import { RelationshipSearchEntities, useRelationships } from '../common/useRelationships'; import { NewTrackedEntityRelationship } from './NewTrackedEntityRelationship'; import { useTrackedEntityTypeName } from './hooks/useTrackedEntityTypeName'; import { useRelationshipTypes } from '../common/RelationshipsWidget/useRelationshipTypes'; +import { LabelKeys, useTermLabel } from '../../../metaData'; export const WidgetTrackedEntityRelationship = ({ relationshipTypes: cachedRelationshipTypes, @@ -27,6 +28,7 @@ export const WidgetTrackedEntityRelationship = ({ }: WidgetTrackedEntityRelationshipProps) => { const { data: relationshipTypes } = useRelationshipTypes(cachedRelationshipTypes); const { data: trackedEntityTypeName, isLoading: isLoadingTEType } = useTrackedEntityTypeName(trackedEntityTypeId); + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular], { programId }); const { data: relationships, isError, @@ -44,7 +46,10 @@ export const WidgetTrackedEntityRelationship = ({ if (isError) { return (
- {i18n.t('Something went wrong while loading relationships. Please try again later.')} + {i18n.t( + 'Something went wrong while loading {{relationshipLabel}}. Please try again later.', + { relationshipLabel }, + )}
); } @@ -55,8 +60,9 @@ export const WidgetTrackedEntityRelationship = ({ return ( = { tableCell: { @@ -28,6 +29,7 @@ export const DeleteRelationshipPlain = ({ classes, }: Props & WithStyles) => { const [isModalOpen, setIsModalOpen] = useState(false); + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular]); return ( <> @@ -48,11 +50,19 @@ export const DeleteRelationshipPlain = ({ onClose={() => setIsModalOpen(false)} dataTest={'delete-relationship-modal'} > - {i18n.t('Delete relationship')} + + {i18n.t('Delete {{relationshipLabel}}', { relationshipLabel })} + - {i18n.t('Deleting the relationship is permanent and cannot be undone.')} + {i18n.t( + 'Deleting the {{relationshipLabel}} is permanent and cannot be undone.', + { relationshipLabel }, + )} {' '} - {i18n.t('Are you sure you want to delete this relationship?')} + {i18n.t( + 'Are you sure you want to delete this {{relationshipLabel}}?', + { relationshipLabel }, + )} @@ -69,7 +79,7 @@ export const DeleteRelationshipPlain = ({ setIsModalOpen(false); }} > - {i18n.t('Yes, delete relationship')} + {i18n.t('Yes, delete {{relationshipLabel}}', { relationshipLabel })} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/useDeleteRelationship.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/DeleteRelationship/useDeleteRelationship.ts index 52d4e73289..293a214901 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 { LabelKeys, useTermLabel } from '../../../../../metaData'; type Props = { sourceId: string; @@ -27,8 +28,9 @@ const deleteRelationshipMutation = { export const useDeleteRelationship = ({ sourceId }: Props): { onDeleteRelationship: OnDeleteRelationship } => { const dataEngine = useDataEngine(); const queryClient = useQueryClient(); + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular]); const { show: showError } = useAlert( - i18n.t('An error occurred while deleting the relationship.'), + i18n.t('An error occurred while deleting the {{relationshipLabel}}.', { relationshipLabel }), { critical: true, }, diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx index 8e1d74bbe3..639d6582c5 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/LinkedEntityTableBody.component.tsx @@ -1,3 +1,4 @@ +import i18n from '@dhis2/d2-i18n'; import React, { type ComponentType } from 'react'; import { withStyles } from 'capture-core-utils/styles'; import type { WithStyles } from 'capture-core-utils/styles'; @@ -7,11 +8,11 @@ import { DataTableCell, Tooltip, } from '@dhis2/ui'; -import i18n from '@dhis2/d2-i18n'; import { convertServerToClient } from '../../../../converters'; import { convert as convertClientToList } from '../../../../converters/clientToList'; import type { Props } from './linkedEntityTableBody.types'; import { DeleteRelationship } from './DeleteRelationship'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; const styles: Readonly = { row: { @@ -30,73 +31,81 @@ const LinkedEntityTableBodyPlain = ({ context, onDeleteRelationship, classes, -}: Props & WithStyles) => ( - - { - linkedEntities - .map(({ id: entityId, values, baseValues, navigation }) => { - const { pendingApiResponse, relationshipId } = baseValues || {}; - return ( - - { - columns.map(({ id, type, options, convertValue }: any) => { - const value = type ? - convertClientToList(convertServerToClient(values[id], type), type, options) : - convertValue(baseValues?.[id] ?? context.display[id]); +}: Props & WithStyles) => { + const { relationshipLabel } = useTermLabel([LabelKeys.relationshipSingular]); + return ( + + { + linkedEntities + .map(({ id: entityId, values, baseValues, navigation }) => { + const { pendingApiResponse, relationshipId } = baseValues || {}; + return ( + + { + columns.map(({ id, type, options, convertValue }: any) => { + const value = type ? + convertClientToList(convertServerToClient(values[id], type), type, options) : + convertValue(baseValues?.[id] ?? context.display[id]); - return ( - - {({ onMouseOver, onMouseOut, ref }) => ( - !pendingApiResponse && - onLinkedRecordClick({ ...context.navigation, ...navigation } as any) - } - // @ts-expect-error - UI library expects a ref prop, - // but it is not defined in the types - ref={(tableCell) => { - if (tableCell) { - if (pendingApiResponse) { - tableCell.onmouseover = onMouseOver; - tableCell.onmouseout = onMouseOut; - ref.current = tableCell; - } else { - tableCell.onmouseover = null; - tableCell.onmouseout = null; - } + return ( + + {({ onMouseOver, onMouseOut, ref }) => ( + !pendingApiResponse && + onLinkedRecordClick({ + ...context.navigation, + ...navigation, + } as Parameters[0]) } - }} - > - {value} - - )} - - ); - })} - {context.display.showDeleteButton ? ( - - onDeleteRelationship({ relationshipId: relationshipId! }) - } - disabled={pendingApiResponse} - /> - ) : null} - - ); - }) - } - -); + // @ts-expect-error - UI library expects a ref prop, + // but it is not defined in the types + ref={(tableCell) => { + if (tableCell) { + if (pendingApiResponse) { + tableCell.onmouseover = onMouseOver; + tableCell.onmouseout = onMouseOut; + ref.current = tableCell; + } else { + tableCell.onmouseover = null; + tableCell.onmouseout = null; + } + } + }} + > + {value} + + )} + + ); + })} + {context.display.showDeleteButton ? ( + + onDeleteRelationship({ relationshipId: relationshipId! }) + } + disabled={pendingApiResponse} + /> + ) : null} + + ); + }) + } + + ); +}; export const LinkedEntityTableBody = withStyles(styles)(LinkedEntityTableBodyPlain) as ComponentType; diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/RelationshipsWidget.component.tsx index 7948fbb2ae..eea32adfe0 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 @@ -1,5 +1,5 @@ -import React, { type ComponentType, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { type ComponentType, useState } from 'react'; import { colors, spacersNum } from '@dhis2/ui'; import { withStyles } from 'capture-core-utils/styles'; import type { WithStyles } from 'capture-core-utils/styles'; @@ -10,6 +10,7 @@ import { LinkedEntitiesViewer } from './LinkedEntitiesViewer.component'; import type { Props } from './relationshipsWidget.types'; import { LoadingMaskElementCenter } from '../../../LoadingMasks'; import { useDeleteRelationship } from './DeleteRelationship/useDeleteRelationship'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; const styles = { header: {}, @@ -38,6 +39,9 @@ const RelationshipsWidgetPlain = ({ const [open, setOpenStatus] = useState(true); const groupedLinkedEntities = useGroupedLinkedEntities(sourceId, relationshipTypes, relationships, readOnly); const { onDeleteRelationship } = useDeleteRelationship({ sourceId }); + const { enrollmentLabel, relationshipsLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.relationshipPlural], + ); if (isLoading) { return ( @@ -92,7 +96,10 @@ const RelationshipsWidgetPlain = ({ } {(relationships?.length ?? 0) === 0 && (
- {i18n.t("This enrollment doesn't have any relationships")} + {i18n.t( + "This {{enrollmentLabel}} doesn't have any {{relationshipsLabel}}", + { enrollmentLabel, relationshipsLabel }, + )}
)} {children} diff --git a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts index bcd69e09cb..244037db7c 100644 --- a/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts +++ b/src/core_modules/capture-core/components/WidgetsRelationship/common/RelationshipsWidget/useGroupedLinkedEntities.ts @@ -3,13 +3,13 @@ import log from 'loglevel'; import moment from 'moment'; import i18n from '@dhis2/d2-i18n'; import { errorCreator } from 'capture-core-utils'; -import { dataElementTypes } from '../../../../metaData'; +import { dataElementTypes, LabelKeys, useTermLabel } from '../../../../metaData'; import { RELATIONSHIP_ENTITIES } from '../constants'; import { convertClientToList, convertServerToClient } from '../../../../converters'; import type { GroupedLinkedEntities, LinkedEntityData } from './types'; import type { ApiLinkedEntity, InputRelationshipData, RelationshipTypes } from '../Types'; -const getFallbackFieldsByRelationshipEntity = { +const getFallbackFieldsByRelationshipEntity = (programStageLabel: string) => ({ [RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE]: () => [{ id: 'trackedEntityTypeName', displayName: i18n.t('Type'), @@ -23,7 +23,7 @@ const getFallbackFieldsByRelationshipEntity = { }], [RELATIONSHIP_ENTITIES.PROGRAM_STAGE_INSTANCE]: () => [{ id: 'programStageName', - displayName: i18n.t('Program stage name'), + displayName: i18n.t('{{programStageLabel}} name', { programStageLabel }), convertValue: (programStageName: any) => programStageName, }, { @@ -33,9 +33,9 @@ const getFallbackFieldsByRelationshipEntity = { convertServerToClient(createdDate, dataElementTypes.DATE), dataElementTypes.DATE, ), }], -}; +}); -const getColumns = ({ relationshipEntity, trackerDataView }: any) => { +const getColumns = ({ relationshipEntity, trackerDataView }: any, programStageLabel: string) => { let fields; if (relationshipEntity === RELATIONSHIP_ENTITIES.TRACKED_ENTITY_INSTANCE) { fields = trackerDataView.attributes; @@ -44,7 +44,7 @@ const getColumns = ({ relationshipEntity, trackerDataView }: any) => { } if (!fields?.length) { - fields = getFallbackFieldsByRelationshipEntity[relationshipEntity](); + fields = getFallbackFieldsByRelationshipEntity(programStageLabel)[relationshipEntity](); } return fields; @@ -169,73 +169,76 @@ export const useGroupedLinkedEntities = ( relationshipTypes: RelationshipTypes | null | undefined, relationships?: Array, readOnly?: boolean, -): GroupedLinkedEntities => useMemo(() => { - if (!relationships?.length || !relationshipTypes?.length) { - return []; - } - - return relationships - .sort((a, b) => moment(b.createdAt) - .diff(moment(a.createdAt))) - .reduce((accGroupedLinkedEntities, relationship) => { - const { - relationship: relationshipId, - relationshipType: relationshipTypeId, - from: fromEntity, - to: toEntity, - pendingApiResponse, - createdAt: relationshipCreatedAt, - } = relationship; - - const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); - if (!relationshipType) { - log.error( - errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), - ); - return accGroupedLinkedEntities; - } - - const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); - if (!apiLinkedEntity) { - return accGroupedLinkedEntities; - } +): GroupedLinkedEntities => { + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular]); + return useMemo(() => { + if (!relationships?.length || !relationshipTypes?.length) { + return []; + } - if (!relationshipType.bidirectional && apiLinkedEntity === fromEntity) { - return accGroupedLinkedEntities; - } + return relationships + .sort((a, b) => moment(b.createdAt) + .diff(moment(a.createdAt))) + .reduce((accGroupedLinkedEntities, relationship) => { + const { + relationship: relationshipId, + relationshipType: relationshipTypeId, + from: fromEntity, + to: toEntity, + pendingApiResponse, + createdAt: relationshipCreatedAt, + } = relationship; + + const relationshipType = relationshipTypes.find(type => type.id === relationshipTypeId); + if (!relationshipType) { + log.error( + errorCreator('Could not find relationshipType')({ relationshipTypeId, relationshipTypes }), + ); + return accGroupedLinkedEntities; + } + + const apiLinkedEntity = determineLinkedEntity(fromEntity, toEntity, sourceId); + if (!apiLinkedEntity) { + return accGroupedLinkedEntities; + } + + if (!relationshipType.bidirectional && apiLinkedEntity === fromEntity) { + return accGroupedLinkedEntities; + } + + const linkedEntityData = getLinkedEntityData( + apiLinkedEntity, + { relationshipCreatedAt, relationshipId }, + pendingApiResponse); + if (!linkedEntityData) { + return accGroupedLinkedEntities; + } + + const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; + const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); + if (group) { + group.linkedEntities = [ + ...group.linkedEntities, + linkedEntityData, + ]; + } else { + const { constraint, name } = apiLinkedEntity === fromEntity ? + { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : + { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; + + const columns = getColumns(constraint, programStageLabel); + const context = getContext(constraint, relationshipType.access, readOnly); + + accGroupedLinkedEntities.push({ + id: groupId, + name: name || relationshipType.displayName, + linkedEntities: [linkedEntityData], + columns, + context, + }); + } - const linkedEntityData = getLinkedEntityData( - apiLinkedEntity, - { relationshipCreatedAt, relationshipId }, - pendingApiResponse); - if (!linkedEntityData) { return accGroupedLinkedEntities; - } - - const groupId = `${relationshipTypeId}-${apiLinkedEntity === fromEntity ? 'from' : 'to'}`; - const group = accGroupedLinkedEntities.find(({ id }) => id === groupId); - if (group) { - group.linkedEntities = [ - ...group.linkedEntities, - linkedEntityData, - ]; - } else { - const { constraint, name } = apiLinkedEntity === fromEntity ? - { constraint: relationshipType.fromConstraint, name: relationshipType.toFromName } : - { constraint: relationshipType.toConstraint, name: relationshipType.fromToName }; - - const columns = getColumns(constraint); - const context = getContext(constraint, relationshipType.access, readOnly); - - accGroupedLinkedEntities.push({ - id: groupId, - name: name || relationshipType.displayName, - linkedEntities: [linkedEntityData], - columns, - context, - }); - } - - return accGroupedLinkedEntities; - }, [] as any); -}, [relationships, relationshipTypes, sourceId, readOnly]); + }, [] as any); + }, [relationships, relationshipTypes, sourceId, readOnly, programStageLabel]); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx index 742e9f6bab..4b6ad01e50 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/ReduxProvider/EventWorkingListsReduxProvider.container.tsx @@ -45,8 +45,8 @@ export const EventWorkingListsReduxProvider = ({ storeId, program, programStage, }, [dispatch, contextOrgUnitId]); const onDeleteEvent = useCallback((eventId: string) => { - dispatch(requestDeleteEvent(eventId, storeId)); - }, [dispatch, storeId]); + dispatch(requestDeleteEvent(eventId, storeId, program.id)); + }, [dispatch, storeId, program.id]); const getLockedFilters = useCallback((selectedTemplate: any) => { if (!selectedTemplate.isDefault) { diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/DeleteEventModal.tsx index 517fd592e1..f6873e6a0c 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,14 +1,17 @@ import React from 'react'; import i18n from '@dhis2/d2-i18n'; import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } from '@dhis2/ui'; +import { LabelKeys, useTermLabel } from '../../../../metaData'; type Props = { eventId: string; + programId: string; onClose: () => void; onConfirmDelete: (eventId: string) => void; }; -export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) => { +export const DeleteEventModal = ({ eventId, programId, onClose, onConfirmDelete }: Props) => { + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId }); const handleConfirm = () => { onConfirmDelete(eventId); onClose(); @@ -20,13 +23,13 @@ export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) = small > - {i18n.t('Delete event')} + {i18n.t('Delete {{eventLabel}}', { eventLabel })}

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

@@ -40,7 +43,7 @@ export const DeleteEventModal = ({ eventId, onClose, onConfirmDelete }: Props) = destructive onClick={handleConfirm} > - {i18n.t('Yes, delete event')} + {i18n.t('Yes, delete {{eventLabel}}', { eventLabel })} diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx index b85a4c0299..43e67c3d82 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/RowMenuSetup/EventWorkingListsRowMenuSetup.component.tsx @@ -1,5 +1,5 @@ -import React, { useMemo, useState } from 'react'; import i18n from '@dhis2/d2-i18n'; +import React, { useMemo, useState } from 'react'; import { IconDelete24, colors } from '@dhis2/ui'; import { EventWorkingListsUpdateTrigger } from '../UpdateTrigger'; import type { CustomRowMenuContents } from '../../WorkingListsBase'; @@ -7,10 +7,11 @@ import type { Props } from './eventWorkingListsRowMenuSetup.types'; import { useProgramExpiryForUser } from '../../../../hooks'; import { isValidPeriod } from '../../../../utils/validation/validators/form'; import { DeleteEventModal } from './DeleteEventModal'; - +import { LabelKeys, useTermLabel } from '../../../../metaData'; export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...passOnProps }: Props) => { const expiryPeriod = useProgramExpiryForUser(programId); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId }); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [eventIdToDelete, setEventIdToDelete] = useState(null); @@ -32,15 +33,15 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas key: 'deleteEventItem', clickHandler: ({ id }) => handleOpenDeleteModal(id), icon: , - label: i18n.t('Delete event'), + label: i18n.t('Delete {{eventLabel}}', { eventLabel }), tooltipContent: (row) => { const { occurredAt } = row ?? {}; const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); return isWithinValidPeriod ? null : i18n.t( - '{{occurredAt}} belongs to an expired period. Event cannot be deleted', + '{{occurredAt}} belongs to an expired period. {{eventLabel}} cannot be deleted', { occurredAt, - interpolation: { escapeValue: false }, + eventLabel, }, ); }, @@ -54,7 +55,7 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas const { isWithinValidPeriod } = isValidPeriod(occurredAt, expiryPeriod); return !isWithinValidPeriod; }, - }], [expiryPeriod]); + }], [expiryPeriod, eventLabel]); return ( @@ -67,6 +68,7 @@ export const EventWorkingListsRowMenuSetup = ({ onDeleteEvent, programId, ...pas {deleteModalOpen && eventIdToDelete && ( diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts index 65536e3865..a48b6d2280 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/epics/eventList.epics.ts @@ -120,7 +120,7 @@ export const requestDeleteEventEpic = ( action$.pipe( ofType(actionTypes.EVENT_REQUEST_DELETE), concatMap((action) => { - const { eventId, storeId } = action.payload; + const { eventId, storeId, programId } = action.payload; const deletePromise = mutate({ resource: 'tracker?async=false&importStrategy=DELETE', type: 'create', @@ -131,7 +131,7 @@ export const requestDeleteEventEpic = ( .then(() => deleteEventSuccess(eventId, storeId)) .catch((error) => { log.error(errorCreator('Could not delete event')({ error, eventId })); - return deleteEventError(); + return deleteEventError(programId); }); return from(deletePromise).pipe( diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts index 832e532a64..19c5fb8869 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingLists/eventWorkingLists.actions.ts @@ -12,10 +12,10 @@ export const deleteEventSuccess = (eventId: string, storeId: string) => actionCreator(actionTypes.EVENT_DELETE_SUCCESS)({ eventId, storeId }); export const deleteEventError = - () => actionCreator(actionTypes.EVENT_DELETE_ERROR)(); + (programId: string) => actionCreator(actionTypes.EVENT_DELETE_ERROR)(null, { programId }); export const openViewEventPage = (eventId: string, contextOrgUnitId: string | null | undefined) => actionCreator(actionTypes.VIEW_EVENT_PAGE_OPEN)({ eventId, orgUnitId: contextOrgUnitId }); -export const requestDeleteEvent = (eventId: string, storeId: string) => - actionCreator(actionTypes.EVENT_REQUEST_DELETE)({ eventId, storeId }); +export const requestDeleteEvent = (eventId: string, storeId: string, programId: string) => + actionCreator(actionTypes.EVENT_REQUEST_DELETE)({ eventId, storeId, programId }); diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx index 35de3c8129..4d33683ac9 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/CompleteAction.tsx @@ -6,6 +6,7 @@ import { useBulkCompleteEvents } from './hooks/useBulkCompleteEvents'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import { Widget } from '../../../../../Widget'; import type { Props } from './CompleteAction.types'; +import { getTermLabel, LabelKeys, useTermLabel } from '../../../../../../metaData'; const styles: Readonly = { container: { @@ -21,9 +22,16 @@ const styles: Readonly = { }, }; -const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { +const getTooltipContent = ( + stageDataWriteAccess: boolean | undefined, + bulkDataEntryIsActive: boolean | undefined, + programId: string, +) => { if (!stageDataWriteAccess) { - return i18n.t('You do not have access to complete events'); + const { eventsLabel } = getTermLabel([LabelKeys.eventPlural], { programId }); + return i18n.t('You do not have access to complete {{eventsLabel}}', { + eventsLabel, + }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -42,7 +50,8 @@ const CompleteActionPlain = ({ }: Props & WithStyles) => { const [isCompleteDialogOpen, setIsCompleteDialogOpen] = useState(false); const [openAccordion, setOpenAccordion] = useState(false); - const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive); + const { eventsLabel } = useTermLabel([LabelKeys.eventPlural], { programId }); + const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive, programId); const disabled = Boolean(!stageDataWriteAccess || bulkDataEntryIsActive); const { eventCounts, @@ -81,15 +90,21 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-events-dialog'} > - {i18n.t('Complete events')} + {i18n.t('Complete {{eventsLabel}}', { eventsLabel })} {eventCounts.active > 0 ? - i18n.t('Are you sure you want to complete all active events in selection?') + i18n.t( + 'Are you sure you want to complete all active {{eventsLabel}} in selection?', + { eventsLabel }, + ) : - i18n.t('There are no active events to complete in the current selection.') + i18n.t( + 'There are no active {{eventsLabel}} to complete in the current selection.', + { eventsLabel }, + ) } @@ -124,12 +139,12 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-events-dialog'} > - {i18n.t('Error completing events')} + {i18n.t('Error completing {{eventsLabel}}', { eventsLabel })} - {i18n.t('There was an error completing the events.')} + {i18n.t('There was an error completing the {{eventsLabel}}.', { eventsLabel })} void; removeRowsFromSelection: (rows: Array) => void; - programId?: string; + programId: string; }; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts index 889127fb1b..7f3a8476be 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/CompleteAction/hooks/useBulkCompleteEvents.ts @@ -1,9 +1,10 @@ -import { useCallback, useEffect, useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; +import { useCallback, useEffect, useMemo } from 'react'; import { useMutation } from '@tanstack/react-query'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; type Props = { selectedRows: { [key: string]: boolean }; @@ -23,6 +24,7 @@ export const useBulkCompleteEvents = ({ programId, }: Props) => { const dataEngine = useDataEngine(); + const { eventsLabel } = useTermLabel([LabelKeys.eventPlural], { programId }); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, @@ -75,7 +77,12 @@ export const useBulkCompleteEvents = ({ }), { onError: () => { - showAlert({ message: i18n.t('An error occurred while completing events') }); + showAlert({ + message: i18n.t( + 'An error occurred while completing {{eventsLabel}}', + { eventsLabel }, + ), + }); }, onSuccess: (response, { payload }: any) => { const errorReports = response?.validationReport?.errorReports; diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx index 5eaf520b57..697d0d1ee6 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/Actions/DeleteAction/DeleteAction.tsx @@ -7,10 +7,18 @@ import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator } from 'capture-core-utils'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import type { Props } from './DeleteAction.types'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; -const getTooltipContent = (stageDataWriteAccess?: boolean, bulkDataEntryIsActive?: boolean) => { +const getTooltipContent = ( + stageDataWriteAccess: boolean | undefined, + bulkDataEntryIsActive: boolean | undefined, + eventsLabel: string, +) => { if (!stageDataWriteAccess) { - return i18n.t('You do not have access to delete events'); + return i18n.t( + 'You do not have access to delete {{eventsLabel}}', + { eventsLabel }, + ); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -26,12 +34,13 @@ export const DeleteAction = ({ }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); const dataEngine = useDataEngine(); + const { eventsLabel } = useTermLabel([LabelKeys.eventPlural]); const { show: showAlert } = useAlert( ({ message }) => message, { critical: true }, ); - const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive); + const tooltipContent = getTooltipContent(stageDataWriteAccess, bulkDataEntryIsActive, eventsLabel); const disabled = Boolean(!stageDataWriteAccess || !!bulkDataEntryIsActive); const { mutate: deleteEvents, isLoading }: { mutate: any, isLoading: boolean } = useMutation( @@ -47,7 +56,12 @@ 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 {{eventsLabel}}', + { eventsLabel }, + ), + }); }, onSuccess: () => { onUpdateList(); @@ -78,13 +92,16 @@ export const DeleteAction = ({ dataTest={'bulk-delete-events-dialog'} > - {i18n.t('Delete events')} + {i18n.t('Delete {{eventsLabel}}', { eventsLabel })} {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 {{eventsLabel}}?', + { eventsLabel }, + )} diff --git a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx index c48eddb0f9..a9ed9fd675 100644 --- a/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/EventWorkingListsCommon/EventBulkActions/EventBulkActions.tsx @@ -24,7 +24,7 @@ export const EventBulkActions = ({ selectedRowsCount={selectedRowsCount} onClearSelection={onClearSelection} > - {programId && onOpenBulkDataEntryPlugin && ( + {onOpenBulkDataEntryPlugin && ( void; removeRowsFromSelection: (rows: Array) => void; - programId?: string; + programId: string; onOpenBulkDataEntryPlugin?: () => void; bulkDataEntryIsActive?: boolean; }; 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..4c611816d2 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, LabelKeys, useTermLabel } 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: i18n.t('{{orgUnitLabel}}', { 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 } = useTermLabel([LabelKeys.orgUnitSingular], { stageId: stage.id }); + return useMemo(() => [ + ...getDefaultMainConfig(stage, orgUnitLabel), ...getMetaDataConfig(stage), - ], [stage]); + ], [stage, orgUnitLabel]); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useDefaultColumnConfig.ts index 21cc49f81c..befc8a0a99 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,16 @@ 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, LabelKeys, type TrackerProgram, useTermLabel } from '../../../../../metaData'; import type { MainColumnConfig, MetadataColumnConfig, TrackerWorkingListsColumnConfigs } from '../../types'; -const getMainConfig = (hasDisplayInReportsAttributes: boolean): Array => +const getMainConfig = (hasDisplayInReportsAttributes: boolean, orgUnitLabel: string): Array => [ { id: 'programOwnerId', visible: false, type: dataElementTypes.ORGANISATION_UNIT, - header: i18n.t('Owner organisation unit'), + header: i18n.t('Owner {{orgUnitLabel}}', { orgUnitLabel }), sortDisabled: true, filterHidden: true, apiViewName: 'programOwner', @@ -27,7 +27,7 @@ const getMainConfig = (hasDisplayInReportsAttributes: boolean): Array => +const getProgramStageMainConfig = (programStage, orgUnitLabel: string, eventLabel: string): Array => [ { id: ADDITIONAL_FILTERS.status, @@ -57,7 +57,7 @@ const getProgramStageMainConfig = (programStage): Array => id: ADDITIONAL_FILTERS.orgUnit, visible: true, type: dataElementTypes.ORGANISATION_UNIT, - header: ADDITIONAL_FILTERS_LABELS.orgUnit, + header: i18n.t('{{eventLabel}} {{orgUnitLabel}}', { eventLabel, orgUnitLabel }), apiViewName: 'eventOrgUnit', }, ...(programStage.enableUserAssignment @@ -141,23 +141,26 @@ export const useDefaultColumnConfig = ( program: TrackerProgram, orgUnitId: string | null | undefined, programStageId: string | null | undefined, -): TrackerWorkingListsColumnConfigs => - useMemo(() => { +): TrackerWorkingListsColumnConfigs => { + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular], { programId: program.id }); + const { eventLabel } = useTermLabel([LabelKeys.eventSingular], { programId: program.id, stageId: programStageId }); + return useMemo(() => { const { attributes, stages } = program; const searchFilterMetaById = buildSearchFilterMetaById(program); const programStage = programStageId && stages.get(programStageId); const hasDisplayInReportsAttributes = attributes.some(attribute => attribute.displayInReports); const defaultColumns = [ - ...getMainConfig(hasDisplayInReportsAttributes), + ...getMainConfig(hasDisplayInReportsAttributes, orgUnitLabel), ...getTEIMetaDataConfig(attributes, orgUnitId, searchFilterMetaById), ]; if (programStageId && programStage) { return defaultColumns.concat([ - ...getProgramStageMainConfig(programStage), + ...getProgramStageMainConfig(programStage, orgUnitLabel, eventLabel), ...getEventsMetaDataConfig(programStage), ]); } return defaultColumns; - }, [orgUnitId, program, programStageId]); + }, [orgUnitId, program, programStageId, orgUnitLabel, eventLabel]); +}; 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 0d4b3eaaea..772bae73f4 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,25 @@ 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, LabelKeys, type TrackerProgram, useTermLabel } from '../../../../../metaData'; import { MAIN_FILTERS } from '../../constants'; export const useFiltersOnly = ( - { enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, + { id: programId, enrollment: { enrollmentDateLabel, incidentDateLabel, showIncidentDate }, stages }: TrackerProgram, programStageId?: string, -) => - useMemo(() => { +) => { + const { enrollmentLabel, followUpLabel } = useTermLabel( + [LabelKeys.enrollmentSingular, LabelKeys.followUpSingular], + { programId }, + ); + return useMemo(() => { const enableUserAssignment = !programStageId && Array.from(stages.values()).find((stage: any) => stage.enableUserAssignment); return [ { id: MAIN_FILTERS.PROGRAM_STATUS, type: dataElementTypes.TEXT, - header: i18n.t('Enrollment status'), + header: i18n.t('{{enrollmentLabel}} status', { enrollmentLabel }), options: [ { text: i18n.t('Active'), value: 'ACTIVE' }, { text: i18n.t('Completed'), value: 'COMPLETED' }, @@ -71,7 +75,7 @@ export const useFiltersOnly = ( { id: MAIN_FILTERS.FOLLOW_UP, type: dataElementTypes.BOOLEAN, - header: i18n.t('Follow up'), + header: i18n.t('{{followUpLabel}}', { followUpLabel }), showInMoreFilters: true, multiValueFilter: false, transformRecordsFilter: (rawFilter: string) => ({ @@ -96,4 +100,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 9c3a40b2a9..61e9f8f44b 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useProgramStageFilters.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/Setup/hooks/useProgramStageFilters.ts @@ -1,7 +1,14 @@ import { useMemo } from 'react'; import i18n from '@dhis2/d2-i18n'; import { statusTypes, translatedStatusTypes } from 'capture-core/events/statusTypes'; -import { type TrackerProgram, type ProgramStage, dataElementTypes, getProgramEventAccess } from '../../../../../metaData'; +import { + type TrackerProgram, + type ProgramStage, + dataElementTypes, + getProgramEventAccess, + LabelKeys, + useTermLabel, +} from '../../../../../metaData'; import { ADDITIONAL_FILTERS, ADDITIONAL_FILTERS_LABELS } from '../../helpers'; const useProgramStageData = (programStageId, stages) => @@ -44,6 +51,7 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: program.stages, ); const options: Array<{ text: string, value: string }> = useProgramStageDropdowOptions(program.stages, program.id); + const { programStageLabel } = useTermLabel([LabelKeys.programStageSingular], { programId: program.id }); return useMemo(() => { const translatedStatus = translatedStatusTypes(); @@ -61,7 +69,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: type: dataElementTypes.DATE, header: occurredAtLabel, disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + tooltipContent: i18n.t('Choose a {{programStageLabel}} to filter by {{label}}', { + programStageLabel, label: occurredAtLabel, interpolation: { escapeValue: false }, }), @@ -91,7 +100,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: { text: translatedStatus.SKIPPED, value: statusTypes.SKIPPED }, ], disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + tooltipContent: i18n.t('Choose a {{programStageLabel}} to filter by {{label}}', { + programStageLabel, label: ADDITIONAL_FILTERS_LABELS.status, interpolation: { escapeValue: false }, }), @@ -106,7 +116,8 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: type: dataElementTypes.DATE, header: scheduledAtLabel, disabled: !programStageId, - tooltipContent: i18n.t('Choose a program stage to filter by {{label}}', { + tooltipContent: i18n.t('Choose a {{programStageLabel}} to filter by {{label}}', { + programStageLabel, label: scheduledAtLabel, interpolation: { escapeValue: false }, }), @@ -144,5 +155,5 @@ export const useProgramStageFilters = (program: TrackerProgram, programStageId?: ] : []), ]; - }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options, enableUserAssignment]); + }, [programStageId, occurredAtLabel, scheduledAtLabel, hideDueDate, options, enableUserAssignment, programStageLabel]); }; 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..b9156ee93d 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 { useMemo } from 'react'; import type { WorkingListTemplate } from '../../../WorkingListsBase'; +import { LabelKeys, useTermLabel } from '../../../../../metaData'; -export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => - useMemo( +export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | undefined, defaultTemplateId: string) => { + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural]); + return useMemo( () => [ defaultAlteredTemplate || { id: defaultTemplateId, @@ -18,7 +20,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'active', - name: i18n.t('Active enrollments'), + name: i18n.t('Active {{enrollmentsLabel}}', { enrollmentsLabel }), order: 1, access: { update: false, @@ -32,7 +34,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'complete', - name: i18n.t('Completed enrollments'), + name: i18n.t('Completed {{enrollmentsLabel}}', { enrollmentsLabel }), order: 2, access: { update: false, @@ -46,7 +48,7 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, { id: 'cancelled', - name: i18n.t('Cancelled enrollments'), + name: i18n.t('Cancelled {{enrollmentsLabel}}', { enrollmentsLabel }), order: 3, access: { update: false, @@ -59,5 +61,6 @@ export const useStaticTemplates = (defaultAlteredTemplate: WorkingListTemplate | }, }, ], - [defaultAlteredTemplate, defaultTemplateId], + [defaultAlteredTemplate, defaultTemplateId, enrollmentsLabel], ); +}; diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx index be75d43236..2bbc16e1f5 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/CompleteAction.tsx @@ -16,6 +16,7 @@ import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import { useCompleteBulkEnrollments } from './hooks/useCompleteBulkEnrollments'; import { Widget } from '../../../../../Widget'; import type { PlainProps } from './CompleteAction.types'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; const styles: Readonly = { container: { @@ -36,9 +37,13 @@ const styles: Readonly = { }, }; -const getTooltipContent = (programDataWriteAccess: boolean, bulkDataEntryIsActive: boolean) => { +const getTooltipContent = ( + programDataWriteAccess: boolean, + bulkDataEntryIsActive: boolean, + enrollmentsLabel: string, +) => { if (!programDataWriteAccess) { - return i18n.t('You do not have access to bulk complete enrollments'); + return i18n.t('You do not have access to bulk complete {{enrollmentsLabel}}', { enrollmentsLabel }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -59,6 +64,11 @@ const CompleteActionPlain = ({ const [modalIsOpen, setModalIsOpen] = useState(false); const [completeEvents, setCompleteEvents] = useState(true); const [openAccordion, setOpenAccordion] = useState(false); + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { enrollmentsLabel, eventsLabel } = useTermLabel( + [LabelKeys.enrollmentPlural, LabelKeys.eventPlural], + { programId }, + ); const { completeEnrollments, enrollmentCounts, @@ -75,7 +85,7 @@ const CompleteActionPlain = ({ onUpdateList, removeRowsFromSelection, }); - const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive); + const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive, enrollmentsLabel); const disabled = !programDataWriteAccess || bulkDataEntryIsActive; const ModalTextContent = () => { @@ -96,8 +106,12 @@ const CompleteActionPlain = ({ {hasPartiallyUploadedEnrollments ? // eslint-disable-next-line max-len - i18n.t('Some enrollments were completed successfully, but there was an error while completing the rest. Please see the details below.') : - i18n.t('There was an error while completing the enrollments. Please see the details below.') + i18n.t('Some {{enrollmentsLabel}} were completed successfully, but there was an error while completing the rest. Please see the details below.', { enrollmentsLabel }) : + i18n.t( + // eslint-disable-next-line max-len + 'There was an error while completing the {{enrollmentsLabel}}. Please see the details below.', + { enrollmentsLabel }, + ) } @@ -129,7 +143,10 @@ const CompleteActionPlain = ({ if (errorFetchingTrackedEntities) { return (
- {i18n.t('An unexpected error occurred while fetching the enrollments. Please try again.')} + {i18n.t( + 'An unexpected error occurred while fetching the {{enrollmentsLabel}}. Please try again.', + { enrollmentsLabel }, + )}
); } @@ -138,35 +155,50 @@ const CompleteActionPlain = ({ if (enrollmentCounts.active === 0) { return (
- {i18n.t('There are currently no active enrollments in the selection.')} + {i18n.t( + 'There are currently no active {{enrollmentsLabel}} in the selection.', + { enrollmentsLabel }, + )} {' '} - {i18n.t('All enrollments are already completed or cancelled.')} + {i18n.t( + 'All {{enrollmentsLabel}} are already completed or cancelled.', + { enrollmentsLabel }, + )}
); } return (
- {i18n.t('This action will complete {{count}} active enrollment in your selection.', + {i18n.t('This action will complete {{count}} active {{enrollmentLabel}} in your selection.', { count: enrollmentCounts.active, - defaultValue: 'This action will complete {{count}} active enrollment in your selection.', - defaultValue_plural: 'This action will complete {{count}} active enrollments in your selection.', + enrollmentLabel, + defaultValue: 'This action will complete {{count}} active {{enrollmentLabel}} in your selection.', + // eslint-disable-next-line max-len + defaultValue_plural: 'This action will complete {{count}} active {{enrollmentsLabel}} in your selection.', + enrollmentsLabel, }) } {' '} {enrollmentCounts.completed > 0 && - i18n.t('{{count}} enrollment already marked as completed will not be changed.', { + i18n.t('{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', { count: enrollmentCounts.completed, - defaultValue: '{{count}} enrollment already marked as completed will not be changed.', - defaultValue_plural: '{{count}} enrollments already marked as completed will not be changed.', + enrollmentLabel, + defaultValue: '{{count}} {{enrollmentLabel}} already marked as completed will not be changed.', + // eslint-disable-next-line max-len + defaultValue_plural: '{{count}} {{enrollmentsLabel}} already marked as completed will not be changed.', + enrollmentsLabel, }) } setCompleteEvents(prevState => !prevState)} /> @@ -186,7 +218,7 @@ const CompleteActionPlain = ({ disabled={disabled} onClick={() => setModalIsOpen(true)} > - {i18n.t('Complete enrollments')} + {i18n.t('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -196,8 +228,8 @@ const CompleteActionPlain = ({ dataTest={'bulk-complete-enrollments-dialog'} > - {validationError ? i18n.t('Error completing enrollments') - : i18n.t('Complete enrollments')} + {validationError ? i18n.t('Error completing {{enrollmentsLabel}}', { enrollmentsLabel }) + : i18n.t('Complete {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -215,7 +247,10 @@ const CompleteActionPlain = ({ {!validationError && ( diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts index aa7388e5bc..ff16a86687 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/CompleteAction/hooks/useCompleteBulkEnrollments.ts @@ -1,12 +1,13 @@ +import i18n from '@dhis2/d2-i18n'; import { useEffect, useMemo } from 'react'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import i18n from '@dhis2/d2-i18n'; import log from 'loglevel'; import { errorCreator } from 'capture-core-utils'; import { ReactQueryAppNamespace, useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; import type { ProgramStage } from '../../../../../../../metaData'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; type Props = { selectedRows: Record; @@ -93,6 +94,7 @@ export const useCompleteBulkEnrollments = ({ ({ message }) => message, { critical: true }, ); + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural], { programId }); const removeQueries = () => { queryClient.removeQueries( @@ -160,7 +162,7 @@ export const useCompleteBulkEnrollments = ({ }, onError: (serverResponse, variables) => { removeQueries(); - showAlert({ message: i18n.t('An error occurred when completing the enrollments') }); + showAlert({ message: i18n.t('An error occurred when completing the {{enrollmentsLabel}}', { enrollmentsLabel }) }); // eslint-disable-line max-len log.error( errorCreator('An error occurred when completing enrollments')({ serverResponse, @@ -185,7 +187,7 @@ export const useCompleteBulkEnrollments = ({ onUpdateList(true); }, onError: (serverResponse, variables) => { - showAlert({ message: i18n.t('An error occurred when completing the enrollments') }); + showAlert({ message: i18n.t('An error occurred when completing the {{enrollmentsLabel}}', { enrollmentsLabel }) }); // eslint-disable-line max-len log.error( errorCreator('An error occurred when completing enrollments')({ serverResponse, @@ -219,7 +221,8 @@ export const useCompleteBulkEnrollments = ({ serverResponse, enrollments, })); - showAlert({ message: i18n.t('An unknown error occurred when completing enrollments') }); + // eslint-disable-next-line max-len + showAlert({ message: i18n.t('An unknown error occurred when completing {{enrollmentsLabel}}', { enrollmentsLabel }) }); return; } const validEnrollments = filterValidEnrollments(enrollments, errors); diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx index f6d1f5051a..86ce89a9d0 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/DeleteEnrollmentsAction.tsx @@ -5,10 +5,15 @@ import { useAuthority } from '../../../../../../utils/userInfo/useAuthority'; import { EnrollmentDeleteModal } from './EnrollmentDeleteModal'; import { ConditionalTooltip } from '../../../../../Tooltips/ConditionalTooltip'; import type { PlainProps } from './DeleteEnrollmentsAction.types'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; -const getTooltipContent = (programDataWriteAccess: boolean, bulkDataEntryIsActive: boolean) => { +const getTooltipContent = ( + programDataWriteAccess: boolean, + bulkDataEntryIsActive: boolean, + enrollmentsLabel: string, +) => { if (!programDataWriteAccess) { - return i18n.t('You do not have access to delete enrollments'); + return i18n.t('You do not have access to delete {{enrollmentsLabel}}', { enrollmentsLabel }); } if (bulkDataEntryIsActive) { return i18n.t('There is a bulk data entry with unsaved changes'); @@ -27,7 +32,8 @@ export const DeleteEnrollmentsAction = ({ }: PlainProps) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const { hasAuthority } = useAuthority({ authority: CASCADE_DELETE_TEI_AUTHORITY }); - const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive); + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural], { programId }); + const tooltipContent = getTooltipContent(programDataWriteAccess, bulkDataEntryIsActive, enrollmentsLabel); const disabled = !programDataWriteAccess || bulkDataEntryIsActive; if (!hasAuthority) { @@ -45,7 +51,7 @@ export const DeleteEnrollmentsAction = ({ disabled={disabled} onClick={() => setIsDeleteDialogOpen(true)} > - {i18n.t('Delete enrollments')} + {i18n.t('Delete {{enrollmentsLabel}}', { enrollmentsLabel })} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx index 67b3b227f1..c2813fb738 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/EnrollmentDeleteModal/EnrollmentDeleteModal.tsx @@ -5,6 +5,7 @@ import i18n from '@dhis2/d2-i18n'; import { useDeleteEnrollments } from '../hooks/useDeleteEnrollments'; import { CustomCheckbox } from './CustomCheckbox'; import type { PlainProps } from './EnrollmentDeleteModal.types'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; const styles: Readonly = { modalContent: { @@ -26,6 +27,11 @@ const EnrollmentDeleteModalPlain = ({ setIsDeleteDialogOpen, classes, }: PlainProps & WithStyles) => { + const { enrollmentLabel } = useTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { enrollmentsLabel, eventsLabel } = useTermLabel( + [LabelKeys.enrollmentPlural, LabelKeys.eventPlural], + { programId }, + ); const { deleteEnrollments, isDeletingEnrollments, @@ -49,12 +55,15 @@ const EnrollmentDeleteModalPlain = ({ small > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })}
- {i18n.t('An error occurred while loading the selected enrollments. Please try again.')} + {i18n.t( + 'An error occurred while loading the selected {{enrollmentsLabel}}. Please try again.', + { enrollmentsLabel }, + )}
@@ -78,7 +87,7 @@ const EnrollmentDeleteModalPlain = ({ onClose={() => setIsDeleteDialogOpen(false)} > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })} @@ -107,24 +116,27 @@ const EnrollmentDeleteModalPlain = ({ dataTest={'bulk-delete-enrollments-dialog'} > - {i18n.t('Delete selected enrollments')} + {i18n.t('Delete selected {{enrollmentsLabel}}', { enrollmentsLabel })}
{/* eslint-disable-next-line max-len */} - {i18n.t('This action will permanently delete the selected enrollments, including all associated data and events.')} + {i18n.t('This action will permanently delete the selected {{enrollmentsLabel}}, including all associated data and {{eventsLabel}}.', { enrollmentsLabel, eventsLabel })}
- {i18n.t('Please select which enrollment statuses you want to delete:')} + {i18n.t( + 'Please select which {{enrollmentLabel}} statuses you want to delete:', + { enrollmentLabel }, + )}
- {i18n.t('Delete {{count}} enrollment', { + {i18n.t('Delete {{count}} {{enrollmentLabel}}', { count: numberOfEnrollmentsToDelete, - defaultValue: 'Delete {{count}} enrollment', - defaultValue_plural: 'Delete {{count}} enrollments', + enrollmentLabel, + defaultValue: 'Delete {{count}} {{enrollmentLabel}}', + defaultValue_plural: 'Delete {{count}} {{enrollmentsLabel}}', + enrollmentsLabel, })} diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts index 0275cf6fc0..1f60810dfa 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteEnrollmentsAction/hooks/useDeleteEnrollments.ts @@ -1,11 +1,12 @@ +import i18n from '@dhis2/d2-i18n'; import { useCallback, useMemo, useState } from 'react'; import log from 'loglevel'; -import i18n from '@dhis2/d2-i18n'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useAlert, useDataEngine } from '@dhis2/app-runtime'; import { errorCreator } from 'capture-core-utils'; import { handleAPIResponse, REQUESTED_ENTITIES } from '../../../../../../../utils/api'; import { ReactQueryAppNamespace, useApiDataQuery } from '../../../../../../../utils/reactQueryHelpers'; +import { LabelKeys, useTermLabel } from '../../../../../../../metaData'; type Props = { selectedRows: Record; @@ -33,6 +34,7 @@ export const useDeleteEnrollments = ({ ({ message }) => message, { critical: true }, ); + const { enrollmentsLabel } = useTermLabel([LabelKeys.enrollmentPlural], { programId }); const updateStatusToDelete = useCallback((status: string) => { setStatusToDelete(prevStatus => ({ @@ -81,7 +83,12 @@ export const useDeleteEnrollments = ({ { onError: (error) => { log.error(errorCreator('An error occurred when deleting enrollments')({ error })); - showAlert({ message: i18n.t('An error occurred when deleting enrollments') }); + showAlert({ + message: i18n.t( + 'An error occurred when deleting {{enrollmentsLabel}}', + { enrollmentsLabel }, + ), + }); }, onSuccess: () => { queryClient.removeQueries([ReactQueryAppNamespace, ...QueryKey]); diff --git a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx index 44daa45d42..9dfaef189f 100644 --- a/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx +++ b/src/core_modules/capture-core/components/WorkingLists/TrackerWorkingLists/TrackedEntityBulkActions/Actions/DeleteTeiAction/DeleteTeiAction.tsx @@ -4,6 +4,7 @@ import { Button, ButtonStrip, Modal, ModalActions, ModalContent, ModalTitle } fr import { useAuthority } from '../../../../../../utils/userInfo/useAuthority'; import { useCascadeDeleteTei } from './hooks/useCascadeDeleteTei'; import type { PlainProps } from './DeleteTeiAction.types'; +import { LabelKeys, useTermLabel } from '../../../../../../metaData'; const CASCADE_DELETE_TEI_AUTHORITY = 'F_TEI_CASCADE_DELETE'; @@ -17,6 +18,9 @@ export const DeleteTeiAction = ({ }: PlainProps) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const { hasAuthority } = useAuthority({ authority: CASCADE_DELETE_TEI_AUTHORITY }); + const { enrollmentsLabel, eventsLabel } = useTermLabel( + [LabelKeys.enrollmentPlural, LabelKeys.eventPlural], + ); const { deleteTeis, isLoading } = useCascadeDeleteTei({ selectedRows, setIsDeleteDialogOpen, @@ -33,8 +37,9 @@ export const DeleteTeiAction = ({ small onClick={() => setIsDeleteDialogOpen(true)} > - {i18n.t('Delete {{ trackedEntityName }} with all enrollments', { + {i18n.t('Delete {{ trackedEntityName }} with all {{enrollmentsLabel}}', { trackedEntityName: trackedEntityName.toLowerCase(), + enrollmentsLabel, })} @@ -53,7 +58,10 @@ export const DeleteTeiAction = ({ - {i18n.t('Deleting records will also delete any associated enrollments and events.')} + {i18n.t( + 'Deleting records will also delete any associated {{enrollmentsLabel}} and {{eventsLabel}}.', + { enrollmentsLabel, eventsLabel }, + )} {' '} {i18n.t('This cannot be undone.')} {' '} diff --git a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts index 8457d90378..c037bb2ad1 100644 --- a/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts +++ b/src/core_modules/capture-core/metaData/TrackedEntityType/TrackedEntityType.ts @@ -6,7 +6,6 @@ import type { SearchGroup } from '../SearchGroup'; import type { DataElement } from '../DataElement'; import type { TeiRegistration } from './TeiRegistration'; import type { Access } from '../Access'; -import type { CustomLabels } from '../helpers/customLabels'; export class TrackedEntityType { _id!: string; @@ -15,11 +14,9 @@ export class TrackedEntityType { _teiRegistration!: TeiRegistration; _attributes!: Array; _searchGroups!: Array; - _customLabels!: CustomLabels; constructor(initFn: ((_this: TrackedEntityType) => void) | null) { this._attributes = []; - this._customLabels = {}; initFn && isFunction(initFn) && initFn(this); } @@ -64,11 +61,4 @@ export class TrackedEntityType { get attributes(): Array { return this._attributes; } - - set customLabels(customLabels: CustomLabels) { - this._customLabels = customLabels; - } - get customLabels(): CustomLabels { - return this._customLabels; - } } diff --git a/src/core_modules/capture-core/metaData/helpers/constants/customLabels.const.ts b/src/core_modules/capture-core/metaData/helpers/constants/customLabels.const.ts new file mode 100644 index 0000000000..9da569f74b --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/constants/customLabels.const.ts @@ -0,0 +1,72 @@ +import i18n from '@dhis2/d2-i18n'; + +export type LabelConfig = { + apiFieldSingular: string; + apiFieldPlural?: string; + defaultSingular: () => string; + defaultPlural?: () => string; +}; + +export const LABELS = { + enrollment: { + apiFieldSingular: 'displayEnrollmentLabel', + apiFieldPlural: 'displayEnrollmentsLabel', + defaultSingular: () => i18n.t('enrollment'), + defaultPlural: () => i18n.t('enrollments'), + }, + event: { + apiFieldSingular: 'displayEventLabel', + apiFieldPlural: 'displayEventsLabel', + defaultSingular: () => i18n.t('event'), + defaultPlural: () => i18n.t('events'), + }, + programStage: { + apiFieldSingular: 'displayProgramStageLabel', + apiFieldPlural: 'displayProgramStagesLabel', + defaultSingular: () => i18n.t('program stage'), + defaultPlural: () => i18n.t('program stages'), + }, + note: { + apiFieldSingular: 'displayNoteLabel', + apiFieldPlural: 'displayNotesLabel', + defaultSingular: () => i18n.t('note'), + defaultPlural: () => i18n.t('notes'), + }, + relationship: { + apiFieldSingular: 'displayRelationshipLabel', + apiFieldPlural: 'displayRelationshipsLabel', + defaultSingular: () => i18n.t('relationship'), + defaultPlural: () => i18n.t('relationships'), + }, + attribute: { + apiFieldSingular: 'displayTrackedEntityAttributeLabel', + apiFieldPlural: 'displayTrackedEntityAttributesLabel', + defaultSingular: () => i18n.t('attribute'), + defaultPlural: () => i18n.t('attributes'), + }, + orgUnit: { + apiFieldSingular: 'displayOrgUnitLabel', + defaultSingular: () => i18n.t('organisation unit'), + }, + followUp: { + apiFieldSingular: 'displayFollowUpLabel', + defaultSingular: () => i18n.t('follow-up'), + }, +} satisfies Record; + +export const LabelKeys = { + enrollmentSingular: 'enrollment', + enrollmentPlural: { key: 'enrollment', plural: true }, + eventSingular: 'event', + eventPlural: { key: 'event', plural: true }, + programStageSingular: 'programStage', + programStagePlural: { key: 'programStage', plural: true }, + noteSingular: 'note', + notePlural: { key: 'note', plural: true }, + relationshipSingular: 'relationship', + relationshipPlural: { key: 'relationship', plural: true }, + attributeSingular: 'attribute', + attributePlural: { key: 'attribute', plural: true }, + orgUnitSingular: 'orgUnit', + followUpSingular: 'followUp', +} as const; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels.ts new file mode 100644 index 0000000000..3266cc270e --- /dev/null +++ b/src/core_modules/capture-core/metaData/helpers/customLabels.ts @@ -0,0 +1,98 @@ +import { useSelector } from 'react-redux'; +import { programCollection } from '../../metaDataMemoryStores'; +import { LABELS, type LabelConfig } from './constants/customLabels.const'; + +export type CustomLabelKey = keyof typeof LABELS; +export type CustomLabels = Record; +export type TermRequest = CustomLabelKey | { key: CustomLabelKey; plural?: boolean }; + +type LabelSource = Record | undefined | null; + +type ProgramScope = { programId: string | null | undefined; stageId?: string | null }; +type OptionalProgramScope = { programId?: string | null; stageId?: string | null }; +type ProgramContainer = { program: LabelSource }; + +const getLabel = (key: CustomLabelKey): LabelConfig => LABELS[key]; + +const ALL_FIELD_NAMES = Object.values(LABELS as Record).flatMap( + ({ apiFieldSingular, apiFieldPlural }) => (apiFieldPlural ? [apiFieldSingular, apiFieldPlural] : [apiFieldSingular]), +); + +const resolveDefault = (key: CustomLabelKey, plural: boolean): string => { + const label = getLabel(key); + return plural ? label.defaultPlural?.() ?? label.defaultSingular() : label.defaultSingular(); +}; + +const resolveLabel = ( + sources: ReadonlyArray, + key: CustomLabelKey, + plural: boolean, +): string => { + const { apiFieldSingular, apiFieldPlural } = getLabel(key); + const target = plural ? apiFieldPlural : apiFieldSingular; + const found = target + ? sources + .map(source => source?.[target]) + .find((value): value is string => typeof value === 'string') + : undefined; + return found ?? resolveDefault(key, plural); +}; + +const resolveFromCollection = ( + programId: string | null | undefined, + stageId: string | null | undefined, + key: CustomLabelKey, + plural: boolean, +): string => { + const program = programId ? programCollection.get(programId) : undefined; + const stage = program && stageId ? program.getStage(stageId) : undefined; + return resolveLabel([stage?.customLabels, program?.customLabels], key, plural); +}; + +const buildLabels = ( + requests: ReadonlyArray, + resolve: (key: CustomLabelKey, plural: boolean) => string, +): CustomLabels => { + const entries = requests.map((req) => { + const isString = typeof req === 'string'; + const key = isString ? req : req.key; + const plural = !isString && (req.plural ?? false); + const outputKey = plural ? `${key}sLabel` : `${key}Label`; + return [outputKey, resolve(key, plural)]; + }); + return Object.fromEntries(entries); +}; + +/** Use in metadata-load code (factories) to pluck label fields from a raw API object. */ +export const extractCustomLabels = (cached: Record): CustomLabels => + Object.fromEntries( + ALL_FIELD_NAMES.flatMap((field) => { + const value = cached[field]; + return typeof value === 'string' ? [[field, value]] : []; + }), + ); + +/** Use outside React (selectors, thunks); reads from `programCollection`. */ +export const getTermLabel = ( + requests: ReadonlyArray, + { programId, stageId }: ProgramScope, +): CustomLabels => + buildLabels(requests, (key, plural) => resolveFromCollection(programId, stageId, key, plural)); + +/** Use in self-contained widgets that already own the program object (no Redux dep). */ +export const getTermLabelFromProgram = ( + requests: ReadonlyArray, + { program }: ProgramContainer, +): CustomLabels => + buildLabels(requests, (key, plural) => resolveLabel([program], key, plural)); + +/** Use inside React components; `programId` falls back to `currentSelections.programId`. */ +export const useTermLabel = ( + requests: ReadonlyArray, + { programId, stageId }: OptionalProgramScope = {}, +): CustomLabels => { + const activeProgramId = useSelector(({ currentSelections }: any) => + programId ?? currentSelections.programId); + return buildLabels(requests, (key, plural) => + resolveFromCollection(activeProgramId, stageId, key, plural)); +}; diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts deleted file mode 100644 index 18938840cd..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/customLabels.ts +++ /dev/null @@ -1,73 +0,0 @@ -type CustomLabelField = { - field?: string, - pluralField?: string, -}; - -export const CUSTOM_LABEL_FIELDS = { - enrollment: { field: 'displayEnrollmentLabel', pluralField: 'displayEnrollmentsLabel' }, - followUp: { field: 'displayFollowUpLabel' }, - orgUnit: { field: 'displayOrgUnitLabel' }, - relationship: { field: 'displayRelationshipLabel' }, - note: { field: 'displayNoteLabel' }, - attribute: { field: 'displayTrackedEntityAttributeLabel' }, - programStage: { field: 'displayProgramStageLabel', pluralField: 'displayProgramStagesLabel' }, - event: { field: 'displayEventLabel', pluralField: 'displayEventsLabel' }, - trackedEntityType: { pluralField: 'displayTrackedEntityTypesLabel' }, -} as const satisfies { [key: string]: CustomLabelField }; - -export type CustomLabelKey = keyof typeof CUSTOM_LABEL_FIELDS; -export type CustomLabels = Record; -export type LabelOptions = { plural?: boolean }; - -const allFields: Array = Array.from( - new Set( - Object.values(CUSTOM_LABEL_FIELDS) - .flatMap((term: CustomLabelField) => [term.field, term.pluralField]) - .filter((field): field is string => Boolean(field)), - ), -); - -export const extractCustomLabels = (cached: Record): CustomLabels => { - const labels: CustomLabels = {}; - allFields.forEach((field) => { - if (cached[field]) { - labels[field] = cached[field]; - } - }); - return labels; -}; - -type LabelSource = CustomLabels | undefined | null; - -export const resolveLabel = ( - sources: LabelSource | Array, - key: CustomLabelKey, - { plural = false }: LabelOptions = {}, -): string | undefined => { - const term: CustomLabelField = CUSTOM_LABEL_FIELDS[key]; - 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); -}; - -type WithLabels = { customLabels?: CustomLabels } | undefined | null; - -export const getProgramLabel = (program: WithLabels, key: CustomLabelKey, options?: LabelOptions): string | undefined => - resolveLabel(program?.customLabels, key, options); - -export const getStageLabel = ( - stage: WithLabels, - program: WithLabels, - key: CustomLabelKey, - options?: LabelOptions, -): string | undefined => resolveLabel([stage?.customLabels, program?.customLabels], key, options); - -export const getTrackedEntityTypeLabel = ( - trackedEntityType: WithLabels, - key: CustomLabelKey, - options?: LabelOptions, -): string | undefined => resolveLabel(trackedEntityType?.customLabels, key, options); diff --git a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts b/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts deleted file mode 100644 index 49b34132fe..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { - CUSTOM_LABEL_FIELDS, - resolveLabel, - extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, -} 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 deleted file mode 100644 index c733c2e662..0000000000 --- a/src/core_modules/capture-core/metaData/helpers/customLabels/useLabel.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { programCollection, trackedEntityTypesCollection } from '../../../metaDataMemoryStores'; -import { resolveLabel } from './customLabels'; -import type { CustomLabelKey, LabelOptions } from './customLabels'; - -type ProgramOptions = LabelOptions & { programId?: string }; -type StageOptions = LabelOptions & { programId?: string, stageId?: string }; -type TrackedEntityTypeOptions = LabelOptions & { tetId?: string }; - -export const useProgramLabel = (key: CustomLabelKey, { programId, plural }: ProgramOptions = {}): string | undefined => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const id = programId ?? currentProgramId; - return useMemo( - () => resolveLabel(id ? programCollection.get(id)?.customLabels : undefined, key, { plural }), - [id, key, plural], - ); -}; - -export const useStageLabel = ( - key: CustomLabelKey, - { programId, stageId, plural }: StageOptions = {}, -): string | undefined => { - const currentProgramId = useSelector(({ currentSelections }: any) => currentSelections.programId); - const currentStageId = useSelector(({ currentSelections }: any) => currentSelections.stageId); - const pId = programId ?? currentProgramId; - const sId = stageId ?? currentStageId; - return useMemo(() => { - const program = pId ? programCollection.get(pId) : undefined; - const stage = program && sId ? program.getStage(sId) : undefined; - return resolveLabel([stage?.customLabels, program?.customLabels], key, { plural }); - }, [pId, sId, key, plural]); -}; - -export const useTrackedEntityTypeLabel = ( - key: CustomLabelKey, - { tetId, plural }: TrackedEntityTypeOptions = {}, -): string | undefined => { - const currentTetId = useSelector(({ currentSelections }: any) => currentSelections.trackedEntityTypeId); - const id = tetId ?? currentTetId; - return useMemo( - () => resolveLabel(id ? trackedEntityTypesCollection.get(id)?.customLabels : undefined, key, { plural }), - [id, key, plural], - ); -}; diff --git a/src/core_modules/capture-core/metaData/helpers/index.ts b/src/core_modules/capture-core/metaData/helpers/index.ts index 627adbd3b3..51ded21356 100644 --- a/src/core_modules/capture-core/metaData/helpers/index.ts +++ b/src/core_modules/capture-core/metaData/helpers/index.ts @@ -18,14 +18,13 @@ export { getProgramEventAccess } from './getProgramEventAccess'; export { getProgramAndStageForProgram } from './getProgramAndStageForProgram'; export { getProgramThrowIfNotFound } from './getProgramThrowIfNotFound'; export { - CUSTOM_LABEL_FIELDS, - resolveLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, - useProgramLabel, - useStageLabel, - useTrackedEntityTypeLabel, + getTermLabel, + getTermLabelFromProgram, + useTermLabel, + type CustomLabelKey, + type CustomLabels, + type TermRequest, } from './customLabels'; -export type { CustomLabelKey, CustomLabels, LabelOptions } from './customLabels'; +export { LabelKeys } from './constants/customLabels.const'; + diff --git a/src/core_modules/capture-core/metaData/index.ts b/src/core_modules/capture-core/metaData/index.ts index 00e7aca7aa..917eca2738 100644 --- a/src/core_modules/capture-core/metaData/index.ts +++ b/src/core_modules/capture-core/metaData/index.ts @@ -40,14 +40,12 @@ export { getProgramThrowIfNotFound, getProgramAndStageForEventProgram, getEventProgramEventAccess, - CUSTOM_LABEL_FIELDS, - resolveLabel, extractCustomLabels, - getProgramLabel, - getStageLabel, - getTrackedEntityTypeLabel, - useProgramLabel, - useStageLabel, - useTrackedEntityTypeLabel, + getTermLabel, + getTermLabelFromProgram, + LabelKeys, + useTermLabel, + type CustomLabelKey, + type CustomLabels, + type TermRequest, } from './helpers'; -export type { CustomLabelKey, CustomLabels, LabelOptions } from './helpers'; diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/programs/factory/programStage/ProgramStageFactory.ts index 112aa41d14..9c4720bec3 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 @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle */ import log from 'loglevel'; +import i18n from '@dhis2/d2-i18n'; import { errorCreator } from 'capture-core-utils'; import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; import { camelCaseUppercaseString } from 'capture-core-utils/string/getCamelCaseFromUppercase'; @@ -239,8 +240,16 @@ export class ProgramStageFactory { _form.description = cachedProgramStage.description; _form.featureType = ProgramStageFactory._getFeatureType(cachedProgramStage); _form.access = cachedProgramStage.access; - _form.addLabel({ id: 'occurredAt', label: cachedProgramStage.displayExecutionDateLabel || 'Report date' }); - _form.addLabel({ id: 'scheduledAt', label: cachedProgramStage.displayDueDateLabel || 'Scheduled date' }); + const executionLabel = cachedProgramStage.displayExecutionDateLabel; + const dueDateLabel = cachedProgramStage.displayDueDateLabel; + _form.addLabel({ + id: 'occurredAt', + label: executionLabel ? capitalizeFirstLetter(executionLabel) : i18n.t('Report date'), + }); + _form.addLabel({ + id: 'scheduledAt', + label: dueDateLabel ? capitalizeFirstLetter(dueDateLabel) : i18n.t('Scheduled date'), + }); _form.validationStrategy = cachedProgramStage.validationStrategy && camelCaseUppercaseString(cachedProgramStage.validationStrategy); diff --git a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts index 5513dce0ee..fb8c442ede 100644 --- a/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts +++ b/src/core_modules/capture-core/metaDataMemoryStoreBuilders/trackedEntityTypes/factory/TrackedEntityType/TrackedEntityTypeFactory.ts @@ -1,8 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { - TrackedEntityType, - extractCustomLabels, -} from '../../../../metaData'; +import { TrackedEntityType } from '../../../../metaData'; import { DataElementFactory } from './DataElementFactory'; import { TeiRegistrationFactory } from './TeiRegistrationFactory'; import { SearchGroupFactory } from '../../../common/factory'; @@ -84,7 +81,6 @@ export class TrackedEntityTypeFactory { o.name = this._getTranslation( cachedType.translations, TrackedEntityTypeFactory.translationPropertyNames.NAME) || cachedType.displayName; - o.customLabels = extractCustomLabels(cachedType); }); if (cachedType.trackedEntityTypeAttributes) { diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts index 929645433c..8e226e4880 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/storePrograms.ts @@ -1,3 +1,4 @@ +import { FEATURES, featureAvailable } from 'capture-core-utils/featuresSupport'; import { quickStore } from '../../IOUtils'; import { getContext } from '../../context'; import type { CachedProgramStageDataElement } from '../../../storageControllers'; @@ -97,7 +98,7 @@ const programTrackedEntityAttributeFields = [ 'allowFutureDate', ].join(','); -const programStageFields = [ +const baseProgramStageFields = [ 'id', 'access', 'autoGenerateEvent', @@ -117,7 +118,6 @@ const programStageFields = [ 'displayDueDateLabel', 'displayProgramStageLabel', 'displayEventLabel', - 'displayEventsLabel', 'formType', 'featureType', 'validationStrategy', @@ -126,9 +126,13 @@ const programStageFields = [ 'dataEntryForm[id,htmlCode]', 'programStageSections[id,displayName,displayDescription,sortOrder,dataElements[id]]', `programStageDataElements[${programStageDataElementFields}]`, -].join(','); +]; + +const pluralProgramStageFields = [ + 'displayEventsLabel', +]; -const fieldsParam = [ +const baseProgramFields = [ 'id', 'displayName', 'displayShortName', @@ -138,16 +142,13 @@ const fieldsParam = [ 'displayIncidentDateLabel', 'displayEnrollmentDateLabel', 'displayEnrollmentLabel', - 'displayEnrollmentsLabel', 'displayFollowUpLabel', 'displayOrgUnitLabel', 'displayRelationshipLabel', 'displayNoteLabel', 'displayTrackedEntityAttributeLabel', 'displayProgramStageLabel', - 'displayProgramStagesLabel', 'displayEventLabel', - 'displayEventsLabel', 'minAttributesRequiredToSearch', 'useFirstStageDuringRegistration', 'onlyEnrollOnce', @@ -163,16 +164,38 @@ const fieldsParam = [ 'access[data[read,write]]', 'trackedEntityType[id]', 'categoryCombo[id,displayName,isDefault,categories[id,displayName]]', - `programStages[${programStageFields}]`, 'programSections[id, displayDescription, displayFormName, sortOrder, trackedEntityAttributes]', `programTrackedEntityAttributes[${programTrackedEntityAttributeFields}]`, -].join(','); +]; + +const pluralProgramFields = [ + 'displayEnrollmentsLabel', + 'displayProgramStagesLabel', + 'displayEventsLabel', + 'displayNotesLabel', + 'displayRelationshipsLabel', + 'displayTrackedEntityAttributesLabel', +]; + +const buildFieldsParam = (includePluralLabels: boolean): string => { + const stageFields = includePluralLabels + ? [...baseProgramStageFields, ...pluralProgramStageFields] + : baseProgramStageFields; + const programFields = includePluralLabels + ? [...baseProgramFields, ...pluralProgramFields] + : baseProgramFields; + return [ + ...programFields, + `programStages[${stageFields.join(',')}]`, + ].join(','); +}; export const storePrograms = (programIds: Array) => { + const includePluralLabels = featureAvailable(FEATURES.customTerminologyPlurals); const query = { resource: 'programs', params: { - fields: fieldsParam, + fields: buildFieldsParam(includePluralLabels), filter: `id:in:[${programIds.join(',')}]`, pageSize: programIds.length, }, diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts index 676c11b68a..4849b10ab2 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/programs/quickStoreOperations/types/apiPrograms.types.ts @@ -147,8 +147,11 @@ type apiProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, + displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, + displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, + displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, diff --git a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts index 1e6f04c161..4271797227 100644 --- a/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts +++ b/src/core_modules/capture-core/metaDataStoreLoaders/trackedEntityTypes/quickStoreOperations/storeTrackedEntityTypes.ts @@ -26,7 +26,8 @@ const convert = (() => { })); })(); -const fieldsParam = 'id,access,displayName,displayTrackedEntityTypesLabel,minAttributesRequiredToSearch,featureType,' + +const FIELDS = + 'id,access,displayName,minAttributesRequiredToSearch,featureType,' + 'trackedEntityTypeAttributes[trackedEntityAttribute[id],displayInList,mandatory,searchable],' + 'translations[property,locale,value]'; @@ -34,7 +35,7 @@ export const storeTrackedEntityTypes = (ids: Array) => { const query = { resource: 'trackedEntityTypes', params: { - fields: fieldsParam, + fields: FIELDS, filter: `id:in:[${ids.join(',')}]`, pageSize: ids.length, }, diff --git a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx index 0b14108661..96cfba8fc7 100644 --- a/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx +++ b/src/core_modules/capture-core/metadataRetrieval/coreOrgUnit/useCoreOrgUnit.tsx @@ -1,10 +1,11 @@ -import React from 'react'; import i18n from '@dhis2/d2-i18n'; +import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useOrgUnitGroups } from 'capture-core/hooks/useOrgUnitGroups'; import { useOrganisationUnit } from '../../dataQueries'; import { orgUnitFetched } from './coreOrgUnit.actions'; import type { CoreOrgUnit } from './coreOrgUnit.types'; +import { LabelKeys, useTermLabel } from '../../metaData'; export function useCoreOrgUnit(orgUnitId: string): { orgUnit?: CoreOrgUnit, @@ -16,6 +17,13 @@ export function useCoreOrgUnit(orgUnitId: string): { // These hooks do no work when id is undefined const { orgUnit, error } = useOrganisationUnit(fetchId, 'displayName,code,path'); const { orgUnitGroups, error: groupError } = useOrgUnitGroups(fetchId); + const { orgUnitLabel } = useTermLabel([LabelKeys.orgUnitSingular]); + + const errorComponent = ( +
+ {i18n.t('{{orgUnitLabel}} could not be retrieved. Please try again later.', { orgUnitLabel })} +
+ ); if (reduxOrgUnit) { return { orgUnit: reduxOrgUnit }; @@ -40,9 +48,3 @@ export function useCoreOrgUnit(orgUnitId: string): { return {}; } - -const errorComponent = ( -
- {i18n.t('organisation unit could not be retrieved. Please try again later.')} -
-); diff --git a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts index b03059cd59..e4aa6d8f56 100644 --- a/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts +++ b/src/core_modules/capture-core/reducers/descriptions/feedback.reducerDescriptionGetter.ts @@ -5,6 +5,7 @@ import isString from 'd2-utilizr/lib/isString'; import isObject from 'd2-utilizr/lib/isObject'; import uuid from 'd2-utilizr/lib/uuid'; import { errorCreator } from 'capture-core-utils'; +import { getTermLabel, LabelKeys } from '../../metaData'; import { createReducerDescription } from '../../trackerRedux/trackerReducer'; import { actionTypes as feedbackActionTypes } from '../../components/FeedbackBar/actions/feedback.actions'; import { actionTypes as dataEntryActionTypes } from '../../components/DataEntry/actions/dataEntry.actions'; @@ -80,12 +81,15 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti const errorMessage = isString(error) ? error : error.message; const errorObject = isObject(error) ? error : null; log.error(errorCreator(errorMessage || 'Error saving event')(errorObject)); - return addErrorFeedback({ message: i18n.t('Could not save event') }); + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: action.meta.programId }); + return addErrorFeedback({ message: i18n.t('Could not save {{eventLabel}}', { eventLabel }) }); }, [workingListsCommonActionTypes.LIST_UPDATE_ERROR]: (_state, action) => addErrorFeedback({ message: action.payload.errorMessage }), - [eventWorkingListsActionTypes.EVENT_DELETE_ERROR]: () => - addErrorFeedback({ message: i18n.t('Could not delete event') }), + [eventWorkingListsActionTypes.EVENT_DELETE_ERROR]: (_state, action) => { + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: action.meta.programId }); + return addErrorFeedback({ message: i18n.t('Could not delete {{eventLabel}}', { eventLabel }) }); + }, [workingListsCommonActionTypes.TEMPLATE_UPDATE_ERROR]: () => addErrorFeedback({ message: i18n.t('Could not save working list') }), [workingListsCommonActionTypes.TEMPLATE_ADD_ERROR]: () => @@ -99,34 +103,79 @@ export const getFeedbackDesc = (appUpdaters: Updaters) => createReducerDescripti const errorMessage = isString(error) ? error : error.message; const errorObject = isObject(error) ? error : null; log.error(errorCreator(errorMessage || 'Error saving event')(errorObject)); - return addErrorFeedback({ message: i18n.t('Could not save event') }); + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: action.meta.programId }); + return addErrorFeedback({ message: i18n.t('Could not save {{eventLabel}}', { eventLabel }) }); }, [dataEntryActionTypes.DATA_ENTRY_RELATIONSHIP_ALREADY_EXISTS]: (_state, action) => addErrorFeedback({ message: action.payload.message }), [viewEventNewRelationshipActionTypes.EVENT_RELATIONSHIP_ALREADY_EXISTS]: (_state, action) => addErrorFeedback({ message: action.payload.message }), [registrationSectionActionTypes.ORG_UNIT_SEARCH_FAILED]: () => - addErrorFeedback({ message: i18n.t('Organisation unit search failed.') }), + addErrorFeedback({ message: i18n.t('Search failed. Please try again.') }), [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error saving tracked entity instance') }), - [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error saving enrollment') }), - [enrollmentSiteActionTypes.SAVE_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error saving the enrollment event') }), - [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error deleting the enrollment event') }), - [editEventDataEntryAction.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED]: () => - addErrorFeedback({ message: i18n.t('Error editing the event, the changes made were not saved') }), + [registrationFormActionTypes.NEW_TRACKED_ENTITY_INSTANCE_WITH_ENROLLMENT_SAVE_FAILED]: (_state, action) => { + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId: action.meta.programId }); + return addErrorFeedback({ + message: i18n.t('Error saving {{enrollmentLabel}}', { enrollmentLabel }), + }); + }, + [enrollmentSiteActionTypes.SAVE_FAILED]: (_state, action) => { + const programId = action.payload.programId; + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); + return addErrorFeedback({ + message: i18n.t( + 'Error saving the {{enrollmentLabel}} {{eventLabel}}', + { enrollmentLabel, eventLabel }, + ), + }); + }, + [editEventActionTypes.DELETE_EVENT_DATA_ENTRY_FAILED]: (_state, action) => { + const programId = action.meta.programId; + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); + return addErrorFeedback({ + message: i18n.t( + 'Error deleting the {{enrollmentLabel}} {{eventLabel}}', + { enrollmentLabel, eventLabel }, + ), + }); + }, + [editEventDataEntryAction.SAVE_EDIT_EVENT_DATA_ENTRY_FAILED]: (_state, action) => { + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId: action.meta.programId }); + return addErrorFeedback({ + message: i18n.t('Error editing the {{eventLabel}}, the changes made were not saved', { eventLabel }), + }); + }, [enrollmentSiteActionTypes.ERROR_ENROLLMENT]: (_state, action) => addErrorFeedback({ message: i18n.t(action.payload.message) }), [viewEventActionTypes.ASSIGNEE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), [enrollmentEditEventActionTypes.ASSIGNEE_SAVE_FAILED]: () => addErrorFeedback({ message: i18n.t('Error updating the Assignee') }), - [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: () => - addErrorFeedback({ message: i18n.t('Could not save enrollment note') }), - [eventNoteActionTypes.ADD_NOTE_FAILED_FOR_EVENT]: () => - addErrorFeedback({ message: i18n.t('Could not save event note') }), - [viewEventNotesActionTypes.SAVE_EVENT_NOTE_FAILED]: () => - addErrorFeedback({ message: i18n.t('Could not save event note') }), + [enrollmentNoteActionTypes.ADD_NOTE_FAILED_FOR_ENROLLMENT]: (_state, action) => { + const programId = action.meta.selections.programId; + const { enrollmentLabel } = getTermLabel([LabelKeys.enrollmentSingular], { programId }); + const { noteLabel } = getTermLabel([LabelKeys.noteSingular], { programId }); + return addErrorFeedback({ + message: i18n.t('Could not save {{enrollmentLabel}} {{noteLabel}}', { enrollmentLabel, noteLabel }), + }); + }, + [eventNoteActionTypes.ADD_NOTE_FAILED_FOR_EVENT]: (_state, action) => { + const programId = action.meta.programId; + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); + const { noteLabel } = getTermLabel([LabelKeys.noteSingular], { programId }); + return addErrorFeedback({ + message: i18n.t('Could not save {{eventLabel}} {{noteLabel}}', { eventLabel, noteLabel }), + }); + }, + [viewEventNotesActionTypes.SAVE_EVENT_NOTE_FAILED]: (_state, action) => { + const programId = action.meta.programId; + const { eventLabel } = getTermLabel([LabelKeys.eventSingular], { programId }); + const { noteLabel } = getTermLabel([LabelKeys.noteSingular], { programId }); + return addErrorFeedback({ + message: i18n.t('Could not save {{eventLabel}} {{noteLabel}}', { eventLabel, noteLabel }), + }); + }, }, 'feedbacks', []); diff --git a/src/core_modules/capture-core/storageControllers/types/cache.types.ts b/src/core_modules/capture-core/storageControllers/types/cache.types.ts index 771a55c14b..8701e50648 100644 --- a/src/core_modules/capture-core/storageControllers/types/cache.types.ts +++ b/src/core_modules/capture-core/storageControllers/types/cache.types.ts @@ -184,7 +184,6 @@ export type CachedTrackedEntityType = { id: string, access: Access, displayName: string, - displayTrackedEntityTypesLabel?: string | null, trackedEntityTypeAttributes?: Array | null, translations: Array, minAttributesRequiredToSearch: number, @@ -215,8 +214,11 @@ export type CachedProgram = { displayFollowUpLabel?: string | null, displayOrgUnitLabel?: string | null, displayRelationshipLabel?: string | null, + displayRelationshipsLabel?: string | null, displayNoteLabel?: string | null, + displayNotesLabel?: string | null, displayTrackedEntityAttributeLabel?: string | null, + displayTrackedEntityAttributesLabel?: string | null, displayProgramStageLabel?: string | null, displayProgramStagesLabel?: string | null, displayEventLabel?: string | null, diff --git a/src/core_modules/capture-core/utils/validation/getValidators.ts b/src/core_modules/capture-core/utils/validation/getValidators.ts index 70baa31049..ca4c11e36c 100644 --- a/src/core_modules/capture-core/utils/validation/getValidators.ts +++ b/src/core_modules/capture-core/utils/validation/getValidators.ts @@ -65,7 +65,7 @@ const errorMessages = { EMAIL: i18n.t('Please provide a valid email address'), AGE: i18n.t('Please provide a valid age'), PHONE_NUMBER: i18n.t('Please provide a valid phone number'), - ORGANISATION_UNIT: i18n.t('Please provide a valid organisation unit'), + ORGANISATION_UNIT: i18n.t('Please provide a valid selection.'), COORDINATE: i18n.t('Please provide valid coordinates'), UNIQUENESS: i18n.t('This value already exists'), RANGE: i18n.t('"From" cannot be greater than "To"'), diff --git a/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx b/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx index 3e56259858..2d8d2495a6 100644 --- a/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx +++ b/src/core_modules/capture-core/utils/warnings/UnsupportedAttributesNotification/UnsupportedAttributesNotification.component.tsx @@ -1,7 +1,8 @@ +import i18n from '@dhis2/d2-i18n'; import React from 'react'; import { withStyles, type WithStyles } from 'capture-core-utils/styles'; -import i18n from '@dhis2/d2-i18n'; import { NoticeBox, spacers } from '@dhis2/ui'; +import { LabelKeys, useTermLabel } from '../../../metaData'; import type { SearchAttribute } from '../../../metaDataMemoryStoreBuilders/common/factory/searchGroup'; const styles: Readonly = (theme: any) => ({ @@ -22,18 +23,20 @@ const UnsupportedAttributesNotificationPlain = ({ unsupportedAttributes, classes, }: Props) => { + const { attributeLabel } = useTermLabel([LabelKeys.attributeSingular]); const message = - i18n.t('The following attribute type is not supported for searching and has been hidden', { + i18n.t('The following {{attributeLabel}} type is not supported for searching and has been hidden', { count: unsupportedAttributes.length, + attributeLabel, defaultValue: - 'The following attribute type is not supported for searching and has been hidden', + 'The following {{attributeLabel}} type is not supported for searching and has been hidden', defaultValue_plural: - 'The following attribute types are not supported for searching and have been hidden', + 'The following {{attributeLabel}} types are not supported for searching and have been hidden', }); return (
- + {message}{': '} {unsupportedAttributes.map((attr, index) => ( diff --git a/src/i18n/setupFormatters.ts b/src/i18n/setupFormatters.ts new file mode 100644 index 0000000000..432bbac1b5 --- /dev/null +++ b/src/i18n/setupFormatters.ts @@ -0,0 +1,49 @@ +/** + * Patches d2-i18n's interpolator for custom terminology. + * + * 1. If the template interpolates any variable listed in CUSTOM_TERM_VARS, + * HTML escaping is disabled for that call. + * All other translations keep default HTML escaping (XSS safety). + * 2. If a custom-term variable is the leading token in a template, its value + * is capitalized (locale-aware). + */ + +import i18n from '@dhis2/d2-i18n'; +import { capitalizeFirstLetter } from 'capture-core-utils/string/capitalizeFirstLetter'; + +const CUSTOM_TERM_VARS = new Set([ + 'enrollmentLabel', 'enrollmentsLabel', + 'eventLabel', 'eventsLabel', + 'programStageLabel', 'programStagesLabel', + 'linkableStageLabel', + 'followUpLabel', 'orgUnitLabel', + 'relationshipLabel', 'relationshipsLabel', + 'noteLabel', 'notesLabel', + 'attributeLabel', 'attributesLabel', + 'trackedEntityLabel', 'trackedEntityTypesLabel', +]); + +const interpolator = (i18n as any).services?.interpolator; +if (interpolator) { + const original = interpolator.interpolate.bind(interpolator); + + interpolator.interpolate = (template: string, data: Record, lng: string, opts: any) => { + const usedVars = [...template.matchAll(/\{\{\s*(\w+)/g)].map(m => m[1]); + if (!usedVars.some(name => CUSTOM_TERM_VARS.has(name))) { + return original(template, data, lng, opts); + } + + const leading = /^\{\{\s*(\w+)/.exec(template.trimStart())?.[1]; + const preparedData = leading && CUSTOM_TERM_VARS.has(leading) && typeof data?.[leading] === 'string' + ? { ...data, [leading]: capitalizeFirstLetter(data[leading] as string) } + : (data ?? {}); + + const previousEscape = interpolator.escapeValue; + interpolator.escapeValue = false; + try { + return original(template, preparedData, lng, opts); + } finally { + interpolator.escapeValue = previousEscape; + } + }; +} diff --git a/src/index.tsx b/src/index.tsx index c25ff0c33c..d0f190a285 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import shadowUrl from 'leaflet/dist/images/marker-shadow.png'; import 'regenerator-runtime'; // To fix the 'regeneratorRuntime is not defined' error comming from react-leaflet-search-unpolyfilled import 'capture-core-utils/extensions/asyncForEachArray'; import 'capture-core-utils/extensions/arrayToHashMap'; +import './i18n/setupFormatters'; import './locales'; //eslint-disable-line import { AppStart } from './components/AppStart';