Repository: WeblateOrg/weblate

Issue:
Include list of skipped results of component discovery
### Describe the issue

New docker weblate installation with version 5.2.1. Add Component works correctly. But on saving Component discovery, it shows "No matching components were found!". In the same path, it should have to discover 30 files more.

The setup is the following:

**Regular expression to match translation files against**
web_areapersonal_companyname/Resources/Views/Empleado/(?P\<component>[^/]\*)\\.(?P\<language>[^/.]*)\\.resx

**File format**
.NET resource file

**Customize the component name**
{{ component }}

**Define the monolingual base filename**
web_areapersonal_companyname/Resources/Views/Empleado/{{ component }}.resx

**Language filter**
^[^.]+$

**File mask** 
web_areapersonal_companyname/Resources/Views/Empleado/EmpleadoBandeja.*.resx

**Monolingual base language file** 
web_areapersonal_companyname/Resources/Views/Empleado/EmpleadoBandeja.ca.resx

**Gitlab repository file examples**
https://<span></span>gitlab.companyname.com/informatica/test_weblate/-/blob/develop/web_areapersonal_companyname/Resources/Views/Empleado/EmpleadoBandeja.ca.resx
https://<span></span>gitlab.companyname.com/informatica/test_weblate/-/blob/develop/web_areapersonal_companyname/Resources/Views/Empleado/EmpleadoPerfil.ca.resx


### 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

No response

### Expected behavior

_No response_

### Screenshots

_No response_

### Exception traceback

_No response_

### How do you run Weblate?

Docker container

### Weblate versions

_No response_

### Weblate deploy checks

_No response_

### Additional context

_No response_


Discussion:
> **Define the monolingual base filename** web_areapersonal_companyname/Resources/Views/Empleado/{{ component }}.resx

It is missing `.ca`, I guess the file without a language code doesn't exist.

> **Monolingual base language file** web_areapersonal_companyname/Resources/Views/Empleado/EmpleadoBandeja.ca.resx

This issue has been marked as a question by a Weblate team member. Why? Because it belongs more to the professional [Weblate Care](https://care.weblate.org/) or community [Discussions](https://github.com/WeblateOrg/weblate/discussions) than here. We strive to answer these reasonably fast here, too, but [purchasing the support subscription](https://weblate.org/support/) is more responsible and faster for your business. And it makes Weblate stronger as well. Thanks!

In case your question is already answered, [making a donation](https://weblate.org/donate/) is the right way to say thank you!
Hi nijel! You are right. Now it works as it is expected and all files are detected. Thank you very much. It would be nice if the web would inform on which field the error is.

I leave my final setup in case it can hep someone else:

**Regular expression to match translation files against**
(?P\<originalHierarchy>.\*/Resources)/(?P\<folders>.\*)/(?P\<component>[^/]\*)\\.(?P\<language>[^/.]\*)\\.resx

**File format**
.NET resource file

**Customize the component name**
{{ folders }}: {{ component }}

**Define the monolingual base filename**
{{ originalHierarchy }}/{{ folders }}/{{ component }}.ca.resx


Thanks for the suggestion, adding a list of skipped entries with a reason would be useful for debugging issues like this. `ComponentDiscovery.check_valid` would return an invalidity reason and `ComponentDiscovery.perform` would return an additional list of skipped entries.
This issue seems to be a good fit for newbie contributors. You are welcome to contribute to Weblate! Don't hesitate to ask any questions you would have while implementing this.

You can learn about how to get started in our [contributors documentation](https://docs.weblate.org/en/latest/contributing/index.html).

Resolving patch:
diff --git a/weblate/addons/forms.py b/weblate/addons/forms.py
--- a/weblate/addons/forms.py
+++ b/weblate/addons/forms.py
@@ -357,7 +357,7 @@ def __init__(self, *args, **kwargs):
             if self.cleaned_data.get("preview"):
                 self.fields["confirm"].widget = forms.CheckboxInput()
                 self.helper.layout.insert(0, Field("confirm"))
-                created, matched, deleted = self.discovery.perform(
+                created, matched, deleted, skipped = self.discovery.perform(
                     preview=True, remove=self.cleaned_data["remove"]
                 )
                 self.helper.layout.insert(
@@ -368,6 +368,7 @@ def __init__(self, *args, **kwargs):
                             "matches_created": created,
                             "matches_matched": matched,
                             "matches_deleted": deleted,
+                            "matches_skipped": skipped,
                             "user": self.user,
                         },
                     ),
diff --git a/weblate/trans/discovery.py b/weblate/trans/discovery.py
--- a/weblate/trans/discovery.py
+++ b/weblate/trans/discovery.py
@@ -9,6 +9,7 @@
 from django.db.models import Q
 from django.utils.functional import cached_property
 from django.utils.text import slugify
+from django.utils.translation import gettext
 
 from weblate.logger import LOGGER
 from weblate.trans.defines import COMPONENT_NAME_LENGTH
@@ -267,39 +268,38 @@ def cleanup(self, main, processed, preview=False):
 
         return deleted
 
-    def check_valid(self, match):
-        def valid_file(name):
-            if not name:
-                return True
-            fullname = os.path.join(self.component.full_path, name)
-            return os.path.exists(fullname)
-
+    def get_skip_reason(self, match):
         # Skip matches to main component
         if match["mask"] == self.component.filemask:
-            return False
-
-        if not valid_file(match["base_file"]):
-            return False
-
-        if not valid_file(match["new_base"]):
-            return False
+            return gettext("File mask matches the main component.")
 
-        if not valid_file(match["intermediate"]):
-            return False
+        for param in ("base_file", "new_base", "intermediate"):
+            name = match[param]
+            if not name:
+                continue
+            fullname = os.path.join(self.component.full_path, name)
+            if not os.path.exists(fullname):
+                return gettext("{filename} ({parameter}) does not exist.").format(
+                    filename=name,
+                    parameter=param,
+                )
 
-        return True
+        return None
 
     def perform(self, preview=False, remove=False, background=False):
         created = []
         matched = []
         deleted = []
+        skipped = []
         processed = set()
 
         main = self.component
 
         for match in self.matched_components.values():
             # Skip invalid matches
-            if not self.check_valid(match):
+            reason = self.get_skip_reason(match)
+            if reason is not None:
+                skipped.append((match, reason))
                 continue
 
             try:
@@ -319,4 +319,4 @@ def perform(self, preview=False, remove=False, background=False):
         if remove:
             deleted = self.cleanup(main, processed, preview)
 
-        return created, matched, deleted
+        return created, matched, deleted, skipped


Test patch:
diff --git a/weblate/trans/tests/test_discovery.py b/weblate/trans/tests/test_discovery.py
--- a/weblate/trans/tests/test_discovery.py
+++ b/weblate/trans/tests/test_discovery.py
@@ -116,24 +116,27 @@ def test_matched_components(self):
     def test_perform(self):
         # Preview should not create anything
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = self.discovery.perform(preview=True)
+            created, matched, deleted, skipped = self.discovery.perform(preview=True)
         self.assertEqual(len(created), 3)
         self.assertEqual(len(matched), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 1)
 
         # Create components
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = self.discovery.perform()
+            created, matched, deleted, skipped = self.discovery.perform()
         self.assertEqual(len(created), 3)
         self.assertEqual(len(matched), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 1)
 
         # Test second call does nothing
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = self.discovery.perform()
+            created, matched, deleted, skipped = self.discovery.perform()
         self.assertEqual(len(created), 0)
         self.assertEqual(len(matched), 3)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 1)
 
         # Remove some files
         repository = self.component.repository
@@ -153,24 +156,29 @@ def test_perform(self):
 
         # Test component removal preview
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = discovery.perform(preview=True, remove=True)
+            created, matched, deleted, skipped = discovery.perform(
+                preview=True, remove=True
+            )
         self.assertEqual(len(created), 0)
         self.assertEqual(len(matched), 1)
         self.assertEqual(len(deleted), 2)
+        self.assertEqual(len(skipped), 1)
 
         # Test component removal
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = discovery.perform(remove=True)
+            created, matched, deleted, skipped = discovery.perform(remove=True)
         self.assertEqual(len(created), 0)
         self.assertEqual(len(matched), 1)
         self.assertEqual(len(deleted), 2)
+        self.assertEqual(len(skipped), 1)
 
         # Components should be now removed
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = discovery.perform(remove=True)
+            created, matched, deleted, skipped = discovery.perform(remove=True)
         self.assertEqual(len(created), 0)
         self.assertEqual(len(matched), 1)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 1)
 
     def test_duplicates(self):
         # Create all components with desired name po
@@ -182,10 +190,11 @@ def test_duplicates(self):
             file_format="po",
         )
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = discovery.perform()
+            created, matched, deleted, skipped = discovery.perform()
         self.assertEqual(len(created), 3)
         self.assertEqual(len(matched), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 1)
 
     def test_multi_language(self):
         discovery = ComponentDiscovery(
@@ -196,11 +205,12 @@ def test_multi_language(self):
             file_format="po",
         )
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = discovery.perform()
+            created, matched, deleted, skipped = discovery.perform()
         self.assertEqual(len(created), 1)
         self.assertEqual(created[0][0]["mask"], "localization/*/component.*.po")
         self.assertEqual(len(matched), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 0)
 
     def test_named_group(self):
         discovery = ComponentDiscovery(
@@ -211,10 +221,11 @@ def test_named_group(self):
             file_format="po",
         )
         with override_settings(CREATE_GLOSSARIES=self.CREATE_GLOSSARIES):
-            created, matched, deleted = discovery.perform()
+            created, matched, deleted, skipped = discovery.perform()
         self.assertEqual(len(created), 1)
         self.assertEqual(created[0][0]["mask"], "localization/*/component.*.po")
         self.assertEqual(created[0][0]["name"], "localization: component")
         self.assertEqual(len(matched), 0)
         self.assertEqual(len(deleted), 0)
         self.assertEqual(len(deleted), 0)
+        self.assertEqual(len(skipped), 0)