From 90e9696cad85b8b65ba3c6d1b386ec5d0bc8e414 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 28 Jan 2026 09:46:13 +0100 Subject: [PATCH 1/3] Sync data integrity translations from YAML --- .../src/main/resources/i18n_global.properties | 174 +++++++-------- sync-data-integrity-i18n.py | 209 ++++++++++++++++++ 2 files changed, 296 insertions(+), 87 deletions(-) create mode 100755 sync-data-integrity-i18n.py diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties b/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties index 6e5172e18a25..f577cd07bd0e 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties @@ -1924,7 +1924,7 @@ data_integrity.data_elements_assigned_to_data_sets_with_different_period_types.s data_integrity.data_elements_assigned_to_data_sets_with_different_period_types.name=Data elements assigned to data sets with different period type data_integrity.data_elements_assigned_to_data_sets_with_different_period_types.description=Lists all data elements that are assigned to at least one data set that has a different period type data_integrity.data_elements_violating_exclusive_group_sets.section=Data Elements -data_integrity.data_elements_violating_exclusive_group_sets.name=Data elements with conflicting exclusive group sets +data_integrity.data_elements_violating_exclusive_group_sets.name=Data elements which belong to multiple groups in a group set data_integrity.data_elements_violating_exclusive_group_sets.description=Lists all data elements that are a members of more than one exclusive data element set belonging to the same data element group set data_integrity.data_elements_violating_exclusive_group_sets.recommendation=Either remove the data element from all but one exclusive group set or consider if the set really should be an exclusive group set data_integrity.data_elements_in_data_set_not_in_form.section=Data Sets @@ -1934,6 +1934,7 @@ data_integrity.data_elements_in_data_set_not_in_form.recommendation=Either remov data_integrity.data_sets_not_assigned_to_org_units.section=Data Sets data_integrity.data_sets_not_assigned_to_org_units.name=Data sets not assigned to organisation units data_integrity.data_sets_not_assigned_to_org_units.description=Lists all data sets that are not assigned to any organisation units +data_integrity.datasets_empty.name=Datasets without data elements data_integrity.category_combos_being_invalid.section=Categories data_integrity.category_combos_being_invalid.name=Invalid Category-combos data_integrity.category_combos_being_invalid.description=Lists all category-combos that are invalid @@ -1952,7 +1953,7 @@ data_integrity.indicators_with_invalid_denominator.section=Indicators data_integrity.indicators_with_invalid_denominator.name=Indicators with invalid denominator data_integrity.indicators_with_invalid_denominator.description=List all indicators where the denominator expression is not valid data_integrity.indicators_violating_exclusive_group_sets.section=Indicators -data_integrity.indicators_violating_exclusive_group_sets.name=Indicators with conflicting exclusive group sets +data_integrity.indicators_violating_exclusive_group_sets.name=Indicators which belong to multiple groups in a group set data_integrity.indicators_violating_exclusive_group_sets.description=Lists all indicators that are members of multiple exclusive indicator groups data_integrity.indicators_violating_exclusive_group_sets.recommendation=Remove the indicator from all but one exclusive indicator group data_integrity.periods_duplicates.section=Periods @@ -1979,7 +1980,7 @@ data_integrity.org_unit_groups_without_group_sets.section=Organisation Units data_integrity.org_unit_groups_without_group_sets.name=Organisation unit groups lacking group sets data_integrity.org_unit_groups_without_group_sets.description=Lists all organisation unit groups that aren't member of at least one organisation unit group set data_integrity.validation_rules_without_groups.section=Validation Rules -data_integrity.validation_rules_without_groups.name=Validation rules lacking groups +data_integrity.validation_rules_without_groups.name=Validation rules without a validation rule group data_integrity.validation_rules_without_groups.description=Lists all validation rules that are not member of at least one validation rule group data_integrity.validation_rules_with_invalid_left_side_expression.section=Validation Rules data_integrity.validation_rules_with_invalid_left_side_expression.name=Validation rules with invalid left side expression @@ -1996,10 +1997,10 @@ data_integrity.program_indicators_with_invalid_filters.name=Program indicators w data_integrity.program_indicators_with_invalid_filters.description=Lists all program indicators with invalid filter expression data_integrity.program_indicators_with_invalid_filters.recommendation=Check that the expression evaluates to true/false data_integrity.program_indicators_without_expression.section=Program Indicators -data_integrity.program_indicators_without_expression.name=Program indicators lacking an expression +data_integrity.program_indicators_without_expression.name=Program indicators without expressions data_integrity.program_indicators_without_expression.description=Lists all program indicators that have no expression set yet data_integrity.program_rules_without_condition.section=Program Rules -data_integrity.program_rules_without_condition.name=Program rules lacking a condition +data_integrity.program_rules_without_condition.name=Program rules without rule conditions. data_integrity.program_rules_without_condition.description=Lists all programs with rules that have no condition yet data_integrity.program_rules_without_priority.section=Program Rules data_integrity.program_rules_without_priority.name=Program rules lacking a priority setting @@ -2031,99 +2032,98 @@ data_integrity.program_rule_actions_without_stage_id.name=Program rules with act data_integrity.program_rule_actions_without_stage_id.description=Lists program rules connected to an action of a type that requires a program stage but is not yet connected to one #YAML based data integrity checks data_integrity.categories_no_options.name=Categories with no category options -data_integrity.categories_dimensions_no_visualizations.name = Categories which are dimensions but which do not have any associated visualizations. -data_integrity.categories_one_default_category.name=Only one default category should exist -data_integrity.categories_one_default_category_combo.name=Only one default category combo should exist -data_integrity.categories_one_default_category_option.name=Only one default category option should exist -data_integrity.categories_one_default_category_option_combo.name=Only one default category option combo should exist -data_integrity.categories_same_category_options.name=Categories with the same category options -data_integrity.categories_unique_category_combo.name=Different category combinations should not have the exact same combination of categories. -data_integrity.invalid_category_combos.name=Invalid category combinations +data_integrity.categories_dimensions_no_visualizations.name=Categories which are enabled as a data dimension but which have no associated visualizations +data_integrity.categories_one_default_category.name=Only one "default" category should exist +data_integrity.categories_one_default_category_combo.name=Only one "default" category combo should exist +data_integrity.categories_one_default_category_option.name=Only one "default" category option should exist +data_integrity.categories_one_default_category_option_combo.name=Only one "default" category option combo should exist +data_integrity.categories_same_category_options.name=Categories with identical category options +data_integrity.categories_unique_category_combo.name=Category combinations with identical categories +data_integrity.invalid_category_combos.name=Category combinations which are invalid data_integrity.category_combos_unused.name=Category combinations not used by other metadata objects -data_integrity.category_option_combos_disjoint.name=Category option combinations with disjoint associations. -data_integrity.category_option_combos_no_names.name=Category option combinations with no names -data_integrity.category_option_combos_have_duplicates.name=Category option combination duplicates (same category options and same category combo) -data_integrity.category_option_group_sets_incomplete.name=Category option group sets which do not contain all category options. -data_integrity.category_options_excess_groupset_membership.name=Category options which belong to multiple groups in a category option group set. -data_integrity.category_options_no_categories.name=Category options with no categories. -data_integrity.category_options_shared_within_category_combo.name=Category combinations with categories which share the same category options. -data_integrity.catoptioncombos_no_catcombo.name=Category options combinations with no category combination. -data_integrity.cocs_wrong_cardinality.name=Category option combinations with incorrect cardinality. -data_integrity.dashboards_no_items.name=Dashboards with no items. -data_integrity.data_elements_aggregate_abandoned.name=Aggregate data elements that have not been changed in last 100 days and do not have any data values. -data_integrity.data_elements_can_aggregate_with_none_operator.name=Aggregate data elements which can be aggregated but have an aggregation operator set to NONE. -data_integrity.data_elements_cannot_aggregate_operator_not_none.name=Data elements which cannot be aggregated but have an aggregation operator set to something other than NONE. -data_integrity.data_elements_aggregate_no_analysis.name=Aggregate data elements not used in any favourites (directly or through indicators) -data_integrity.data_elements_aggregate_no_data.name=Aggregate data elements with NO data values. -data_integrity.data_elements_aggregate_no_groups.name=Aggregate data elements not in any data element groups. -data_integrity.data_elements_aggregate_with_different_period_types.name=Aggregate data elements which belong to data sets with different period types. -data_integrity.data_elements_without_datasets.name=Aggregate data elements not assigned to any data sets -data_integrity.datasets_empty.name=Data sets with no data elements -data_integrity.datasets_same_name.name=Data sets with the same name or short name -data_integrity.datasets_custom_data_entry_forms_empty.name=Datasets which have custom data entry forms which are empty. -data_integrity.datasets_not_assigned_to_org_units.name=Data sets not assigned to any organisation units +data_integrity.category_option_combos_disjoint.name=Category option combinations with disjoint associations +data_integrity.category_option_combos_no_names.name=Category option combinations without names +data_integrity.category_option_combos_have_duplicates.name=Category option combos with identical category options and category combos +data_integrity.category_option_group_sets_incomplete.name=Category option group sets which which do not contain all category options +data_integrity.category_options_excess_groupset_membership.name=Category options which belong to multiple groups in a category option group set +data_integrity.category_options_no_categories.name=Category options without categories +data_integrity.category_options_shared_within_category_combo.name=Category combinations with categories with identical category options +data_integrity.catoptioncombos_no_catcombo.name=Category options combinations without category combinations +data_integrity.cocs_wrong_cardinality.name=Category option combinations with incorrect cardinality +data_integrity.dashboards_no_items.name=Dashboards with no items +data_integrity.data_elements_aggregate_abandoned.name=Aggregate data elements not changed in the last 100 days and do not have any data values +data_integrity.data_elements_can_aggregate_with_none_operator.name=Aggregate domain data elements which can be aggregated but have an aggregation operator set to NONE +data_integrity.data_elements_cannot_aggregate_operator_not_none.name=Aggregate data elements which cannot be aggregated but have an aggregation operator not set to NONE +data_integrity.data_elements_aggregate_no_analysis.name=Aggregate data elements not used in any favourites directly or through indicators +data_integrity.data_elements_aggregate_no_data.name=Aggregate data elements without data values +data_integrity.data_elements_aggregate_no_groups.name=Aggregate data elements not in any data element groups +data_integrity.data_elements_aggregate_with_different_period_types.name=Aggregate data elements which belong to data sets with different period types +data_integrity.data_elements_without_datasets.name=Aggregate data elements which are not a member of any dataset +data_integrity.datasets_same_name.name=Datasets with duplicate name or short name +data_integrity.datasets_custom_data_entry_forms_empty.name=Datasets which have custom data entry forms which are empty +data_integrity.datasets_not_assigned_to_org_units.name=Data sets which have not been assigned to any organisation units data_integrity.data_elements_excess_groupset_membership.name=Data elements which belong to multiple groups in a group set. -data_integrity.category_option_group_sets_scarce.name=Category option groups sets should have at least two members. -data_integrity.category_option_groups_scarce.name=Category option groups should have at least two members. -data_integrity.data_element_groups_scarce.name=Data element groups should have at least two members. -data_integrity.indicator_group_sets_scarce.name=Indicator groups sets should have at least two members. -data_integrity.indicator_groups_scarce.name=Indicator groups should have at least two members. -data_integrity.orgunit_groups_scarce.name=Organisation unit groups should have at least two members. -data_integrity.program_indicator_groups_scarce.name=Program indicator groups should have at least two members. -data_integrity.user_groups_scarce.name=User groups should have at least two members. -data_integrity.validation_rule_groups_scarce.name=Validation rule should have at least two members. -data_integrity.indicator_no_analysis.name=Indicators not used in analytical objects. -data_integrity.indicator_types_duplicated.name=Indicator types with the same factor. -data_integrity.indicators_duplicated_terms.name=Indicators with the same terms. -data_integrity.indicators_exact_duplicates.name=Indicators with the same formula. -data_integrity.indicators_not_grouped.name=Indicators not in any groups. -data_integrity.option_sets_wrong_sort_order.name=Option sets with possibly wrong sort order. +data_integrity.category_option_group_sets_scarce.name=Category option group sets should have at least two members +data_integrity.category_option_groups_scarce.name=Category option groups should have at least two members +data_integrity.data_element_groups_scarce.name=Data element groups should have at least two members +data_integrity.indicator_group_sets_scarce.name=Indicator groups sets should have at least two members +data_integrity.indicator_groups_scarce.name=Indicator groups should have at least two members +data_integrity.orgunit_groups_scarce.name=Organisation unit groups should have at least two members +data_integrity.program_indicator_groups_scarce.name=Program indicator groups should have at least two members +data_integrity.user_groups_scarce.name=User groups should have at least two members +data_integrity.validation_rule_groups_scarce.name=Validation rule groups should have at least two members +data_integrity.indicator_no_analysis.name=Indicators not used in any analytical objects +data_integrity.indicator_types_duplicated.name=Indicator types with identical factors +data_integrity.indicators_duplicated_terms.name=Indicators with identical terms +data_integrity.indicators_exact_duplicates.name=Indicators with identical formulas +data_integrity.indicators_not_grouped.name=Indicators not included in any groups +data_integrity.option_sets_wrong_sort_order.name=Option sets with possibly wrong sort order data_integrity.options_sets_empty.name=Empty option sets data_integrity.options_sets_unused.name=Option sets which are not used data_integrity.orgunit_group_sets_excess_groups.name=Organisation units which belong to multiple groups in a group set. data_integrity.orgunits_compulsory_group_count.name=Organisation units that are not in all compulsory orgunit group sets -data_integrity.orgunits_invalid_geometry.name=Organisation units with invalid geometry. -data_integrity.orgunits_multiple_roots.name=The organisation unit hierarchy should have a single root. -data_integrity.orgunits_multiple_spaces.name=Organisation units should not have multiple spaces in their names or shortnames. -data_integrity.orgunits_no_coordinates.name=Organisation units with no coordinates. -data_integrity.orgunits_not_contained_by_parent.name=Organisation units with point coordinates should be contained by their parent. -data_integrity.orgunits_null_island.name=Organisation units located within 100 km of Null Island (0,0). -data_integrity.orgunits_openingdate_gt_closeddate.name=Organisation units which have an opening date later than the closed date. -data_integrity.orgunits_orphaned.name=Orphaned organisation units. -data_integrity.orgunits_same_name_and_parent.name=Organisation units should not have the same name and parent. -data_integrity.orgunits_trailing_spaces.name=Organisation units should not have trailing spaces. -data_integrity.org_units_not_in_compulsory_group_sets.name=Organisation units not in all compulsory organisation unit group sets +data_integrity.orgunits_invalid_geometry.name=Organisation units with invalid geometry +data_integrity.orgunits_multiple_roots.name=Organisation unit hierarchy should have a single root +data_integrity.orgunits_multiple_spaces.name=Organisation units should not have multiple spaces in their names or shortnames +data_integrity.orgunits_no_coordinates.name=Organisation units without coordinates +data_integrity.orgunits_not_contained_by_parent.name=Organisation units with point coordinates should be contained by their parent +data_integrity.orgunits_null_island.name=Organisation units located within 100 km of Null Island (0,0) +data_integrity.orgunits_openingdate_gt_closeddate.name=Organisation units which have an opening date after the closed date +data_integrity.orgunits_orphaned.name=Orphaned organisation units +data_integrity.orgunits_same_name_and_parent.name=Organisation units should not have the same name and parent +data_integrity.orgunits_trailing_spaces.name=Organisation units should not have trailing spaces +data_integrity.org_units_not_in_compulsory_group_sets.name=Orgunits that are not in all compulsory orgunit group sets data_integrity.organisation_units_violating_exclusive_group_sets.name=Organisation units which belong to multiple groups in a group set -data_integrity.organisation_units_without_groups.name=Organisation units not in any groups -data_integrity.periods_3y_future.name=Periods which are more than three years in the future. -data_integrity.periods_distant_past.name=Periods which are in the distant past. +data_integrity.organisation_units_without_groups.name=Organisation units without groups +data_integrity.periods_3y_future.name=Periods which are more than three years in the future +data_integrity.periods_distant_past.name=Periods which are in the distant past data_integrity.periods_same_start_end_date.name=Periods with the same start and end dates data_integrity.periods_same_start_date_period_type.name=Periods with the same start date and period type -data_integrity.programs_custom_data_entry_forms_empty.name=Programs which have custom data entry forms which are empty. -data_integrity.program_stages_no_programs.name=Program stages which are not assigned to any programs. -data_integrity.program_rules_message_no_template.name=Program rules actions which should send or schedule a message without a message template. -data_integrity.program_rules_no_action.name=Program rules with no action. +data_integrity.programs_custom_data_entry_forms_empty.name=Programs which have empty custom data entry forms +data_integrity.program_stages_no_programs.name=Program stages without associated programs +data_integrity.program_rules_message_no_template.name=Program rules actions which should send or schedule a message without a message template +data_integrity.program_rules_no_action.name=Program rules without actions data_integrity.tracker_geometry_invalid_srid.name=Tracker entities with geometry not using SRID 4326. data_integrity.program_rules_no_expression.name=Program rules with no expression. -data_integrity.program_rules_no_priority.name=Program rules with no priority -data_integrity.program_rules_inconsistent_program_program_stage.name=Program rules which are inconsistently linked to a program and program stage. -data_integrity.validation_rules_missing_value_strategy_null.name=All validation rule expressions should have a missing value strategy. -data_integrity.dashboards_not_viewed_one_year.name=Dashboards which have not been actively viewed in the past 12 months -data_integrity.maps_not_viewed_one_year.name=Maps which have not been viewed in the past 12 months -data_integrity.visualizations_not_viewed_one_year.name=Visualizations which have not been viewed in the past 12 months -data_integrity.users_with_invalid_usernames.name = Users with invalid usernames -data_integrity.users_capture_ou_not_in_data_view_ou.name=Users with capture org unit not in data view org unit -data_integrity.users_capture_ou_not_in_tei_search_ou.name=Users with capture org unit not in TEI search org unit -data_integrity.users_with_no_user_role.name=Users with no user role -data_integrity.user_roles_no_authorities.name=User roles with no authorities -data_integrity.user_roles_with_no_users.name=User roles with no users -data_integrity.file_resources_no_icon.name=File resources that are missing an icon -data_integrity.users_with_all_authority.name=Users with the ALL authority -data_integrity.category_options_default_incorrect_sharing.name=Default category option should be publicly shared -data_integrity.tracked_entity_attributes_invalid_trigram_search_configuration.name=Tracked entity attributes eligible for trigram indexing but misconfigured so the partial trigram index is not created. -data_integrity.tracked_entity_attributes_trigram_index_out_of_sync.name=Partial trigram indexes on trackedentityattributevalue table must be in sync with the tracked entity attribute metadata configuration. -data_integrity.tracked_entity_attributes_trigram_index_overview.name=Tracked entity attributes that have a partial trigram index created on the trackedentityattributevalue table. -data_integrity.programs_inconsistent_tracked_entity_type.name=Programs which are inconsistently linked to a tracked entity type. +data_integrity.program_rules_no_priority.name=Program rules without priorities +data_integrity.program_rules_inconsistent_program_program_stage.name=Program rules which are inconsistently linked to a program and program stage +data_integrity.validation_rules_missing_value_strategy_null.name=Validation rule expressions without missing value strategies +data_integrity.dashboards_not_viewed_one_year.name=Dashboards which have not been actively viewed in the last 12 months +data_integrity.maps_not_viewed_one_year.name=Maps which have not been viewed in the last 12 months +data_integrity.visualizations_not_viewed_one_year.name=Visualizations which have not been viewed in the last 12 months +data_integrity.users_with_invalid_usernames.name=Users which have invalid usernames +data_integrity.users_capture_ou_not_in_data_view_ou.name=Users which have a data capture organisation unit which is not in their data view organisation unit hierarchy +data_integrity.users_capture_ou_not_in_tei_search_ou.name=Users which have a data capture organisation unit which is not within their tracked entity search organisation unit hierarchy +data_integrity.users_with_no_user_role.name=Users without user roles +data_integrity.user_roles_no_authorities.name=User roles without assigned authorities +data_integrity.user_roles_with_no_users.name=User roles without assigned users +data_integrity.file_resources_no_icon.name=File resources of type ICON that have no entry in the icon table +data_integrity.users_with_all_authority.name=Users which have the ALL authority assigned +data_integrity.category_options_default_incorrect_sharing.name=The default category option should be publicly shared with all users +data_integrity.tracked_entity_attributes_invalid_trigram_search_configuration.name=Tracked entity attributes having a suboptimal search configuration that potentially results in slower search performance when searching tracked entities using those attributes +data_integrity.tracked_entity_attributes_trigram_index_out_of_sync.name=Partial trigram indexes on trackedentityattributevalue table must be in sync with the tracked entity attribute metadata configuration +data_integrity.tracked_entity_attributes_trigram_index_overview.name=Tracked entity attributes that have a partial trigram index created on the trackedentityattributevalue table +data_integrity.programs_inconsistent_tracked_entity_type.name=Programs which are inconsistently linked to a tracked entity type # -- End Data Integrity Checks--------------------------------------------# diff --git a/sync-data-integrity-i18n.py b/sync-data-integrity-i18n.py new file mode 100755 index 000000000000..c1787dfb7758 --- /dev/null +++ b/sync-data-integrity-i18n.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +Synchronize data integrity i18n entries from YAML check definitions. + +By default runs in dry-run mode and prints a summary. Use --write to update +""" + +from __future__ import annotations + +import argparse +import os +import re +import sys +from typing import List, Tuple, Dict + +DEFAULT_CHECKS_LIST = ( + "dhis-2/dhis-services/dhis-service-administration/src/main/resources/" + "data-integrity-checks.yaml" +) +DEFAULT_I18N = ( + "dhis-2/dhis-services/dhis-service-core/src/main/resources/" + "i18n_global.properties" +) + + +def parse_checks_list(path: str) -> List[str]: + paths: List[str] = [] + with open(path, "r", encoding="utf-8") as f: + for line in f: + match = re.match(r"^\s*-\s+(.+\.yaml)\s*$", line) + if match: + paths.append(match.group(1)) + return paths + + +def _collect_block(lines: List[str], start_index: int, base_indent: int) -> Tuple[str, int]: + parts: List[str] = [] + i = start_index + while i < len(lines): + line = lines[i] + if line.strip() == "": + i += 1 + continue + indent = len(line) - len(line.lstrip(" ")) + if indent <= base_indent: + break + parts.append(line.strip()) + i += 1 + return " ".join(parts).strip(), i + + +def extract_value(lines: List[str], key: str) -> str | None: + pattern = re.compile(rf"^\s*{re.escape(key)}:\s*(.*)$") + i = 0 + while i < len(lines): + line = lines[i] + if line.lstrip().startswith("#"): + i += 1 + continue + match = pattern.match(line) + if match: + value = match.group(1).strip() + if value in {"|", ">", "|-", ">-"}: + base_indent = len(line) - len(line.lstrip(" ")) + block, next_index = _collect_block(lines, i + 1, base_indent) + return block + return value + i += 1 + return None + + +def parse_yaml_name_desc(path: str) -> Tuple[str | None, str | None]: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + name = extract_value(lines, "name") + description = extract_value(lines, "description") + return name, description + + +def load_properties(path: str) -> Tuple[List[str], Dict[str, int]]: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + index: Dict[str, int] = {} + for i, line in enumerate(lines): + if line.lstrip().startswith("#"): + continue + if "=" not in line: + continue + key = line.split("=", 1)[0].strip() + if key: + index[key] = i + return lines, index + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Sync data integrity i18n entries from YAML checks" + ) + parser.add_argument( + "--checks", + default=DEFAULT_CHECKS_LIST, + help="Path to data-integrity-checks.yaml", + ) + parser.add_argument( + "--i18n", + default=DEFAULT_I18N, + help="Path to i18n_global.properties", + ) + parser.add_argument( + "--write", + action="store_true", + help="Write updates to i18n_global.properties", + ) + parser.add_argument( + "--quiet", + action="store_true", + help="Only print errors and final summary", + ) + args = parser.parse_args() + + checks_list_path = args.checks + checks_dir = os.path.join(os.path.dirname(checks_list_path), "data-integrity-checks") + + if not os.path.exists(checks_list_path): + print(f"Checks list not found: {checks_list_path}", file=sys.stderr) + return 2 + if not os.path.exists(args.i18n): + print(f"i18n file not found: {args.i18n}", file=sys.stderr) + return 2 + + rel_paths = parse_checks_list(checks_list_path) + if not rel_paths: + print("No check files found in checks list", file=sys.stderr) + return 2 + + entries: List[Tuple[str, str, str]] = [] + missing_files: List[str] = [] + missing_fields: List[str] = [] + + for rel in rel_paths: + abs_path = os.path.join(checks_dir, rel) + if not os.path.exists(abs_path): + missing_files.append(rel) + continue + name, description = parse_yaml_name_desc(abs_path) + if not name or not description: + missing_fields.append(rel) + continue + entries.append((rel, name, description)) + + lines, index = load_properties(args.i18n) + + updated = 0 + unchanged = 0 + added = 0 + missing_keys: List[str] = [] + changed_keys: List[str] = [] + + for _, name, description in entries: + key = f"data_integrity.{name}.name" + new_line = f"{key}={description}\n" + if key in index: + if lines[index[key]] != new_line: + lines[index[key]] = new_line + updated += 1 + changed_keys.append(key) + else: + unchanged += 1 + else: + missing_keys.append(new_line) + added += 1 + + insert_at = None + for i, line in enumerate(lines): + if line.lstrip().startswith("data_integrity."): + insert_at = i + if insert_at is None: + insert_at = len(lines) - 1 + + if missing_keys: + insert_pos = insert_at + 1 + for new_line in missing_keys: + lines.insert(insert_pos, new_line) + insert_pos += 1 + + if not args.quiet: + for key in changed_keys: + print(f"update: {key}") + for line in missing_keys: + print(f"add: {line.strip()}") + for rel in missing_files: + print(f"missing file: {rel}") + for rel in missing_fields: + print(f"missing fields: {rel}") + + print( + f"updated={updated} added={added} unchanged={unchanged} " + f"missing_files={len(missing_files)} missing_fields={len(missing_fields)}" + ) + + if args.write: + with open(args.i18n, "w", encoding="utf-8") as f: + f.writelines(lines) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 724ea1074b7d03d39f16fd8afa2a36a98a4a9cf3 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 28 Jan 2026 10:07:01 +0100 Subject: [PATCH 2/3] Fix failing tests --- .../src/main/resources/i18n_global.properties | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties b/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties index f577cd07bd0e..aa36e4abf150 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties @@ -1938,11 +1938,7 @@ data_integrity.datasets_empty.name=Datasets without data elements data_integrity.category_combos_being_invalid.section=Categories data_integrity.category_combos_being_invalid.name=Invalid Category-combos data_integrity.category_combos_being_invalid.description=Lists all category-combos that are invalid -data_integrity.category_combos_being_invalid.recommendation=Make sure the category combo is assigned to at least one category and that all categories have category options -data_integrity.indicators_with_identical_formulas.section=Indicators -data_integrity.indicators_with_identical_formulas.name=Indicators with identical formulas -data_integrity.indicators_with_identical_formulas.description=Lists all indicators that have the identical formulas with at least one other indicator -data_integrity.indicators_with_identical_formulas.recommendation=Check if the same indicator should be used everywhere and remove duplicates +data_integrity.category_combos_being_invalid.recommendation=Make sure the category combo is assigned to at least one category and that all categories have category options data_integrity.indicators_without_groups.section=Indicators data_integrity.indicators_without_groups.name=Indicators lacking groups data_integrity.indicators_without_groups.description=Lists all indicators that do not have at least one group @@ -1965,10 +1961,6 @@ data_integrity.org_units_with_cyclic_references.section=Organisation Units data_integrity.org_units_with_cyclic_references.name=Organisation units with cyclic references data_integrity.org_units_with_cyclic_references.description=Lists all organisation units that have a conflicting parent hierarchy definition so that two organisation units become parent of each other or in other words they form a circular reference data_integrity.org_units_with_cyclic_references.recommendation=One of the organisation units must be setup wrongly and needs to be corrected so that a non-circular tree connects the two -data_integrity.org_units_being_orphaned.section=Organisation Units -data_integrity.org_units_being_orphaned.name=Orphaned organisation units -data_integrity.org_units_being_orphaned.description=Lists all organisation units that aren't level 1 units but that lack a parent so that they are not connected to the rest of the organisation tree -data_integrity.org_units_being_orphaned.recommendation=Find (or create) and set the correct parent for the orphaned organisation unit(s). Eventually this is also caused by having set the wrong level. data_integrity.org_units_without_groups.section=Organisation Units data_integrity.org_units_without_groups.name=Organisation units lacking groups data_integrity.org_units_without_groups.description=Lists all organisation units that are not connected to at least one group From acf9e1cd7edf95bcd81bb0b6e46a7a89f9433f4b Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 28 Jan 2026 12:59:23 +0100 Subject: [PATCH 3/3] Fix flaky test --- .../OrganisationUnitControllerTest.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java index 75e8811dea88..d6e51304002c 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import org.hisp.dhis.http.HttpStatus; @@ -225,9 +226,12 @@ void testGetMaxLevel() { @Test void testGetAllOrganisationUnitsByLevel() { - assertEquals( + JsonObject response = + GET("/organisationUnits?levelSorted=true&fields=id,displayName,level").content(); + assertContainsOnly( List.of("L0", "L1x", "L1", "L2x", "L22", "L21", "L3x", "L31", "L32"), - toOrganisationUnitNames(GET("/organisationUnits?levelSorted=true").content())); + toOrganisationUnitNames(response)); + assertTrue(isNonDecreasing(toOrganisationUnitLevels(response))); } @Test @@ -291,4 +295,21 @@ private void assertListOfOrganisationUnits(JsonObject response, String... names) assertContainsOnly(List.of(names), toOrganisationUnitNames(response)); assertEquals(names.length, response.getObject("pager").getNumber("total").intValue()); } + + private List toOrganisationUnitLevels(JsonObject response) { + return response + .getList("organisationUnits", JsonObject.class) + .toList(ou -> ou.getNumber("level").intValue()); + } + + private boolean isNonDecreasing(List values) { + int prev = Integer.MIN_VALUE; + for (int value : values) { + if (value < prev) { + return false; + } + prev = value; + } + return true; + } }