Repository: mozilla/pontoon

Issue:
No warnings when trying to submit empty translations
I've noticed an increase in the number of empty strings in Firefox, where I have [special checks](https://test.flod.org/checks/).

Apparently, we don't warn anymore when someone tries to submit an empty translation.


Discussion:
Is that Fluent specific? We should submit `{ "" }` as an empty translation, which was indeed [introduced recently](https://github.com/mozilla/pontoon/pull/2534).

Is that not the case? What is the expectation?
The expectation is to get a warning, not submit directly. Wasn't that the case before?
I think we prevented submission completely, because the actual empty string was being submitted, which is an error in Fluent.
Note: this is starting to impact other Fluent projects, so it's clear to me that we changed the behavior.
Starting to properly look at this. The proximate cause of this is the way that we no longer construct broken Fluent messages for empty content, so solving the issue will likely require a compare-locales update.

Resolving patch:
diff --git a/pontoon/checks/libraries/pontoon_db.py b/pontoon/checks/libraries/pontoon_db.py
--- a/pontoon/checks/libraries/pontoon_db.py
+++ b/pontoon/checks/libraries/pontoon_db.py
@@ -5,6 +5,7 @@
 
 from collections import defaultdict
 from fluent.syntax import FluentParser, ast
+from fluent.syntax.visitor import Visitor
 
 from pontoon.sync.formats.ftl import localizable_entries
 
@@ -25,6 +26,24 @@ def get_max_length(comment):
     return None
 
 
+class IsEmptyVisitor(Visitor):
+    def __init__(self):
+        self.is_empty = True
+
+    def visit_Placeable(self, node):
+        if isinstance(node.expression, ast.Literal):
+            if node.expression.parse()["value"]:
+                self.is_empty = False
+        elif isinstance(node.expression, ast.SelectExpression):
+            self.generic_visit(node.expression)
+        else:
+            self.is_empty = False
+
+    def visit_TextElement(self, node):
+        if node.value:
+            self.is_empty = False
+
+
 def run_checks(entity, original, string):
     """
     Group all checks related to the base UI that get stored in the DB
@@ -80,4 +99,12 @@ def run_checks(entity, original, string):
         elif entity_ast.id.name != translation_ast.id.name:
             checks["pErrors"].append("Translation key needs to match source string key")
 
+        # Empty translation entry warning; set here rather than pontoon_non_db.py
+        # to avoid needing to parse the Fluent message twice.
+        else:
+            visitor = IsEmptyVisitor()
+            visitor.visit(translation_ast)
+            if visitor.is_empty:
+                checks["pndbWarnings"].append("Empty translation")
+
     return checks


Test patch:
diff --git a/pontoon/checks/tests/test_pontoon_db.py b/pontoon/checks/tests/test_pontoon_db.py
--- a/pontoon/checks/tests/test_pontoon_db.py
+++ b/pontoon/checks/tests/test_pontoon_db.py
@@ -132,6 +132,25 @@ def test_empty_translations(get_entity_mock):
         "pErrors": ["Empty translations are not allowed"]
     }
 
+    assert run_checks(
+        get_entity_mock("ftl", string="key = value"), "", 'key = { "" }'
+    ) == {"pndbWarnings": ["Empty translation"]}
+
+    assert (
+        run_checks(
+            get_entity_mock("ftl", string="key =\n  .attr = value"),
+            "",
+            """key =
+              { $var ->
+                  [a] { "" }
+                 *[b] { "" }
+              }
+              .attr = { "" }
+            """,
+        )
+        == {"pndbWarnings": ["Empty translation"]}
+    )
+
 
 def test_lang_newlines(get_entity_mock):
     """Newlines aren't allowed in lang files"""