Repository: WeblateOrg/weblate

Issue:
Python format check not applied to some plural forms
### Describe the issue

Hello, I have a source string which is a plural using python format (the percent kind). In weblate, I have turned on "python format" as a mandatory check. Somebody just translated the string, and did not use the percent format. There is nothing shown on the string page indicating this is a problem, and this translation is pushed into the repository.

Detailed information below.

### 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. Go to https://translate.fedoraproject.org/translate/anaconda/rhel-9/ar/?q=%25d+_storage+device+selected&sort_by=-priority%2Cposition&checksum=
2. Observe `Flags: python-format`, lack of %d in some of the translation plurals, but no related warning or error.

### Expected behavior

There should be some indication that the check failed.

### Screenshots

_No response_

### Exception traceback

_No response_

### How do you run Weblate?

Other

### Weblate versions

@yurchor can you please fill in the missing parts?

### Weblate deploy checks

_No response_

### Additional context

Detailed information:

- I am using the weblate instance at https://translate.fedoraproject.org, which runs version 4.18.2.
- The string is in Arabic, project `anaconda`, component `rhel-9`: singular ` %d _storage device selected`, plural ` %d _storage devices selected`
- Weblate correctly shows `python-format` in flags, and highlights the `%d`.
- The Arabic translation has these:
  ```
  لا جهازَ تخزينٍ مُحدَّدٌ
  جهازُ تخزينٍ واحدٍ مُحدَّدٌ
  جهازا تخزينٍ اثنان مُحددان
  %d‏ جهاز تخزين محدد
  %d‏ جهاز تخزين محدد
  %d‏ جهاز تخزين محدد
  ```
- When I run `msgfmt -c`, it detects correctly that this particular string's translation is wrong.
- The same string has been a problem already in a different component, the scenario was the same.


Discussion:
Actually, I do not know what can be added here. It is hard to tackle with languages with many plural forms. It is allowed to skip %d for singular but I do not know how to treat this for dual and other forms in Arabic. It should be inspected by Weblate developers.
Sorry, I meant the details the bug template asks about deployment (versions, deploy checks). But maybe that does not matter?
Weblate allows omitting the format string when there is only a single value for the plural form. So for Arabic, it is okay to have six plurals as:

* None storage device selected 
* One storage device selected 
* Two storage devices selected 
* %d storage devices selected 
* %d storage devices selected 
* %d storage devices selected

This is inconsistent with `msgfmt -c`, but allows nicer translations, and most frameworks support this behavior.
Thank you, that answers why this happens.

I suppose there is no way to adjust this on a component level?

Is this check done by something that I can swap with using msgfmt? I found `PythonFormatCheck` in https://github.com/WeblateOrg/weblate/blob/main/weblate/checks/format.py, which does not appear to be easy to isolate, so the answer there is again no?

Resolving patch:
diff --git a/weblate/checks/flags.py b/weblate/checks/flags.py
--- a/weblate/checks/flags.py
+++ b/weblate/checks/flags.py
@@ -42,6 +42,7 @@
 )
 PLAIN_FLAGS["read-only"] = gettext_lazy("Read only")
 PLAIN_FLAGS["strict-same"] = gettext_lazy("Strict unchanged check")
+PLAIN_FLAGS["strict-format"] = gettext_lazy("Strict format string checks")
 PLAIN_FLAGS["forbidden"] = gettext_lazy("Forbidden translation")
 PLAIN_FLAGS["terminology"] = gettext_lazy("Terminology")
 PLAIN_FLAGS["ignore-all-checks"] = gettext_lazy("Ignore all checks")
diff --git a/weblate/checks/format.py b/weblate/checks/format.py
--- a/weblate/checks/format.py
+++ b/weblate/checks/format.py
@@ -297,6 +297,7 @@ def check_generator(self, sources, targets, unit):
             # won't be 0 so don't trigger too many false positives.
             # Some formats do strict linting here, so be strict on those as well.
             len(sources) > 1
+            and "strict-format" not in unit.all_flags
             and (
                 len(plural_examples[0]) == 1
                 or (


Test patch:
diff --git a/weblate/checks/tests/test_format_checks.py b/weblate/checks/tests/test_format_checks.py
--- a/weblate/checks/tests/test_format_checks.py
+++ b/weblate/checks/tests/test_format_checks.py
@@ -998,7 +998,7 @@ def test_reordered_named_template_format_long(self):
 class PluralTest(FixtureTestCase):
     check = PythonFormatCheck()
 
-    def do_check(self, sources, targets, translation):
+    def do_check(self, sources, targets, translation, flags: str = ""):
         return self.check.check_target_unit(
             sources,
             targets,
@@ -1006,6 +1006,7 @@ def do_check(self, sources, targets, translation):
                 translation=translation,
                 source=join_plural(sources),
                 target=join_plural(targets),
+                extra_flags=flags,
             ),
         )
 
@@ -1035,6 +1036,25 @@ def test_arabic(self):
             )
         )
 
+    def test_arabic_strict(self):
+        arabic = Language.objects.get(code="ar")
+        translation = Translation(
+            language=arabic, plural=arabic.plural, component=Component(file_format="po")
+        )
+        self.assertTrue(
+            self.do_check(
+                ["hello %s"] * 2,
+                ["hell"] * 3 + ["hello %s"] * 3,
+                translation,
+                "strict-format",
+            )
+        )
+        self.assertFalse(
+            self.do_check(
+                ["hello %s"] * 2, ["hell %s"] * 6, translation, "strict-format"
+            )
+        )
+
     def test_non_format_singular_fa(self):
         czech = Language.objects.get(code="fa")
         translation = Translation(
@@ -1089,7 +1109,11 @@ def test_non_format_singular(self):
 
     def test_non_format_singular_named(self):
         language = Language.objects.get(code="cs")
-        translation = Translation(language=language, plural=language.plural)
+        translation = Translation(
+            language=language,
+            plural=language.plural,
+            component=Component(file_format="po"),
+        )
         self.assertFalse(
             self.do_check(
                 ["One apple", "%(count)s apples"],
@@ -1114,7 +1138,11 @@ def test_non_format_singular_named(self):
 
     def test_non_format_singular_named_be(self):
         language = Language.objects.get(code="be")
-        translation = Translation(language=language, plural=language.plural)
+        translation = Translation(
+            language=language,
+            plural=language.plural,
+            component=Component(file_format="po"),
+        )
         self.assertTrue(
             self.do_check(
                 ["One apple", "%(count)s apples"],