Repository: WeblateOrg/weblate

Issue:
"reused" translation warning with empty list and resist to reset
### Describe the issue

https://hosted.weblate.org/translate/godot-engine/godot/tr/?q=Create+a+Tile&sort_by=-priority%2Cposition&offset=12

there was a variant of this with small first letters: https://hosted.weblate.org/translate/godot-engine/godot/tr/?q=Create+a+Tile&sort_by=-priority%2Cposition&offset=6

both had the wrong translation.

I corrected the small-lettered first, but missed the size and made it capitalized.

after I corrected the actual capitalized translation, it collided with the other should-be-small-lettered translation and gave me the "reused translation warning".

I marked it to ignore for the time, and continued working with other strings.

I later went back and corrected small-lettered translation.

And Voila! The Bug. now the capitalized version has this empty "other sources" line and it rejects me from resetting it. I press reset, save the string again, change the page and come back. 

The translation does not "resaved" and the "reused" warning is still insisting not to go away.

### I already tried

- [X] I've read and searched [the documentation](https://docs.weblate.org/).
- [X] I've searched for similar issues in this repository.

### Steps to reproduce the behavior

1. have 2 similar strings, A and B
2. translate A  then B to the same content. B should get the "reused" warning
3. press "dismiss" button first
4. the go to A and change it a bit.
5. come back to B and "source" is empty and warning wont go

### Expected behavior

reused warnings on a string must resolve by self if the other string has changed.

### Screenshots

![empty reused translation warningweblate](https://github.com/WeblateOrg/weblate/assets/1812105/e684aede-f461-4af6-9bdf-bcd704181c2a)


### Exception traceback

_No response_

### How do you run Weblate?

weblate.org service

### Weblate versions

_No response_

### Weblate deploy checks

_No response_

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/weblate/checks/base.py b/weblate/checks/base.py
--- a/weblate/checks/base.py
+++ b/weblate/checks/base.py
@@ -31,7 +31,7 @@ class Check:
     source = False
     ignore_untranslated = True
     default_disabled = False
-    propagates = False
+    propagates: str = ""
     param_type = None
     always_display = False
     batch_project_wide = False
diff --git a/weblate/checks/consistency.py b/weblate/checks/consistency.py
--- a/weblate/checks/consistency.py
+++ b/weblate/checks/consistency.py
@@ -4,8 +4,7 @@
 
 from functools import reduce
 
-from django.db.models import Count, Prefetch, Q, Value
-from django.db.models.functions import MD5
+from django.db.models import Count, Prefetch, Q
 from django.utils.translation import gettext, gettext_lazy, ngettext
 
 from weblate.checks.base import TargetCheck
@@ -69,7 +68,7 @@ class ConsistencyCheck(TargetCheck):
         "or is untranslated in some components."
     )
     ignore_untranslated = False
-    propagates = True
+    propagates = "same_source_units"
     batch_project_wide = True
     skip_suggestions = True
 
@@ -142,7 +141,7 @@ class ReusedCheck(TargetCheck):
     check_id = "reused"
     name = gettext_lazy("Reused translation")
     description = gettext_lazy("Different strings are translated the same.")
-    propagates = True
+    propagates = "same_target_units"
     batch_project_wide = True
     skip_suggestions = True
 
@@ -151,21 +150,6 @@ def should_skip(self, unit):
             return True
         return super().should_skip(unit)
 
-    def get_same_target_units(self, unit):
-        from weblate.trans.models import Unit
-
-        translation = unit.translation
-        component = translation.component
-        return Unit.objects.filter(
-            target__md5=MD5(Value(unit.target)),
-            translation__component__project_id=component.project_id,
-            translation__language_id=translation.language_id,
-            translation__component__source_language_id=component.source_language_id,
-            translation__component__allow_translation_propagation=True,
-            translation__plural_id=translation.plural_id,
-            translation__plural__number__gt=1,
-        ).exclude(source__md5=MD5(Value(unit.source)))
-
     def check_target_unit(self, sources, targets, unit):
         translation = unit.translation
         component = translation.component
@@ -176,14 +160,12 @@ def check_target_unit(self, sources, targets, unit):
         if component.batch_checks:
             return self.handle_batch(unit, component)
 
-        return self.get_same_target_units(unit).exists()
+        return unit.same_target_units.exists()
 
     def get_description(self, check_obj):
-        other_sources = (
-            self.get_same_target_units(check_obj.unit)
-            .values_list("source", flat=True)
-            .distinct()
-        )
+        other_sources = check_obj.unit.same_target_units.values_list(
+            "source", flat=True
+        ).distinct()
 
         return ngettext(
             "Other source string: %s", "Other source strings: %s", len(other_sources)
diff --git a/weblate/trans/models/unit.py b/weblate/trans/models/unit.py
--- a/weblate/trans/models/unit.py
+++ b/weblate/trans/models/unit.py
@@ -5,6 +5,7 @@
 from __future__ import annotations
 
 import re
+from functools import reduce
 from typing import TYPE_CHECKING
 
 from django.conf import settings
@@ -1233,14 +1234,14 @@ def run_checks(self, propagate: bool | None = None):  # noqa: C901
                 else:
                     # Create new check
                     create.append(Check(unit=self, dismissed=False, name=check))
-                    needs_propagate |= check_obj.propagates
+                    needs_propagate |= bool(check_obj.propagates)
 
         if create:
             Check.objects.bulk_create(create, batch_size=500, ignore_conflicts=True)
 
         # Propagate checks which need it (for example consistency)
         if (needs_propagate and propagate is not False) or propagate is True:
-            for unit in self.same_source_units:
+            for unit in self.same_source_units | self.same_target_units:
                 try:
                     # Ensure we get a fresh copy of checks
                     # It might be modified meanwhile by propagating to other units
@@ -1258,16 +1259,23 @@ def run_checks(self, propagate: bool | None = None):  # noqa: C901
         if old_checks:
             Check.objects.filter(unit=self, name__in=old_checks).delete()
             propagated_old_checks = []
+            propagated_filters = set()
             for check_name in old_checks:
                 try:
                     if CHECKS[check_name].propagates:
+                        propagated_filters.add(CHECKS[check_name].propagates)
                         propagated_old_checks.append(check_name)
                 except KeyError:
                     # Skip disabled/removed checks
                     continue
+            units = reduce(
+                lambda x, y: x | getattr(self, y),
+                propagated_filters,
+                Unit.objects.none(),
+            )
             if propagated_old_checks:
                 Check.objects.filter(
-                    unit__in=self.same_source_units, name__in=propagated_old_checks
+                    unit__in=units, name__in=propagated_old_checks
                 ).delete()
                 for other in self.same_source_units:
                     other.translation.invalidate_cache()
@@ -1510,6 +1518,20 @@ def same_source_units(self):
             )
         )
 
+    @cached_property
+    def same_target_units(self):
+        translation = self.translation
+        component = translation.component
+        return Unit.objects.filter(
+            target__md5=MD5(Value(self.target)),
+            translation__component__project_id=component.project_id,
+            translation__language_id=translation.language_id,
+            translation__component__source_language_id=component.source_language_id,
+            translation__component__allow_translation_propagation=True,
+            translation__plural_id=translation.plural_id,
+            translation__plural__number__gt=1,
+        ).exclude(source__md5=MD5(Value(self.source)))
+
     def get_max_length(self):
         """Returns maximal translation length."""
         # Fallback to reasonably big value


Test patch: