We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Warn on empty Fluent rich editor fields
> In addition to the bug, I believe this is a UX regression? We should show a warning that one of the values is empty when trying to save such message.

The current practice is to consider any content in a Fluent value or attribute to qualify the whole message as not-empty. The check for this was added in #2675. Previously empty attributes may indeed have resulted in errors, due to stringifying as invalid Fluent.

_Originally posted by @eemeli in https://github.com/mozilla/pontoon/issues/2973#issuecomment-1750344569_
            

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pontoon/checks/libraries/pontoon_db.py`
Content:
```
1 import html
2 import re
3 
4 import bleach
5 
6 from collections import defaultdict
7 from fluent.syntax import FluentParser, ast
8 from fluent.syntax.visitor import Visitor
9 
10 from pontoon.sync.formats.ftl import localizable_entries
11 
12 
13 MAX_LENGTH_RE = re.compile(r"MAX_LENGTH:( *)(\d+)", re.MULTILINE)
14 parser = FluentParser()
15 
16 
17 def get_max_length(comment):
18     """
19     Return max length value for an entity with MAX_LENTH.
20     """
21     max_length = re.findall(MAX_LENGTH_RE, comment or "")
22 
23     if max_length:
24         return int(max_length[0][1])
25 
26     return None
27 
28 
29 class IsEmptyVisitor(Visitor):
30     def __init__(self):
31         self.is_empty = True
32 
33     def visit_Placeable(self, node):
34         if isinstance(node.expression, ast.Literal):
35             if node.expression.parse()["value"]:
36                 self.is_empty = False
37         elif isinstance(node.expression, ast.SelectExpression):
38             self.generic_visit(node.expression)
39         else:
40             self.is_empty = False
41 
42     def visit_TextElement(self, node):
43         if node.value:
44             self.is_empty = False
45 
46 
47 def run_checks(entity, original, string):
48     """
49     Group all checks related to the base UI that get stored in the DB
50     :arg pontoon.base.models.Entity entity: Source entity
51     :arg basestring original: an original string
52     :arg basestring string: a translation
53     """
54     checks = defaultdict(list)
55     resource_ext = entity.resource.format
56 
57     if resource_ext == "lang":
58         # Newlines are not allowed in .lang files (bug 1190754)
59         if "\n" in string:
60             checks["pErrors"].append("Newline characters are not allowed")
61 
62         # Prevent translations exceeding the given length limit
63         max_length = get_max_length(entity.comment)
64 
65         if max_length:
66             string_length = len(
67                 html.unescape(bleach.clean(string, strip=True, tags=()))
68             )
69 
70             if string_length > max_length:
71                 checks["pErrors"].append("Translation too long")
72 
73     # Bug 1599056: Original and translation must either both end in a newline,
74     # or none of them should.
75     if resource_ext == "po":
76         if original.endswith("\n") != string.endswith("\n"):
77             checks["pErrors"].append("Ending newline mismatch")
78 
79     # Prevent empty translation submissions if not supported
80     if string == "" and not entity.resource.allows_empty_translations:
81         checks["pErrors"].append("Empty translations are not allowed")
82 
83     # FTL checks
84     if resource_ext == "ftl" and string != "":
85         translation_ast = parser.parse_entry(string)
86         entity_ast = parser.parse_entry(entity.string)
87 
88         # Parse error
89         if isinstance(translation_ast, ast.Junk):
90             checks["pErrors"].append(translation_ast.annotations[0].message)
91 
92         # Not a localizable entry
93         elif not isinstance(translation_ast, localizable_entries):
94             checks["pErrors"].append(
95                 "Translation needs to be a valid localizable entry"
96             )
97 
98         # Message ID mismatch
99         elif entity_ast.id.name != translation_ast.id.name:
100             checks["pErrors"].append("Translation key needs to match source string key")
101 
102         # Empty translation entry warning; set here rather than pontoon_non_db.py
103         # to avoid needing to parse the Fluent message twice.
104         else:
105             visitor = IsEmptyVisitor()
106             visitor.visit(translation_ast)
107             if visitor.is_empty:
108                 checks["pndbWarnings"].append("Empty translation")
109 
110     return checks
111
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -28,20 +28,27 @@
 
 class IsEmptyVisitor(Visitor):
     def __init__(self):
-        self.is_empty = True
+        self.is_empty = False
+        self.is_pattern_empty = True
+
+    def visit_Pattern(self, node):
+        self.is_pattern_empty = True
+        self.visit(node.elements)
+        if self.is_pattern_empty:
+            self.is_empty = True
 
     def visit_Placeable(self, node):
         if isinstance(node.expression, ast.Literal):
             if node.expression.parse()["value"]:
-                self.is_empty = False
+                self.is_pattern_empty = False
         elif isinstance(node.expression, ast.SelectExpression):
             self.generic_visit(node.expression)
         else:
-            self.is_empty = False
+            self.is_pattern_empty = False
 
     def visit_TextElement(self, node):
         if node.value:
-            self.is_empty = False
+            self.is_pattern_empty = False
 
 
 def run_checks(entity, original, string):

{"golden_diff": "diff --git a/pontoon/checks/libraries/pontoon_db.py b/pontoon/checks/libraries/pontoon_db.py\n--- a/pontoon/checks/libraries/pontoon_db.py\n+++ b/pontoon/checks/libraries/pontoon_db.py\n@@ -28,20 +28,27 @@\n \n class IsEmptyVisitor(Visitor):\n     def __init__(self):\n-        self.is_empty = True\n+        self.is_empty = False\n+        self.is_pattern_empty = True\n+\n+    def visit_Pattern(self, node):\n+        self.is_pattern_empty = True\n+        self.visit(node.elements)\n+        if self.is_pattern_empty:\n+            self.is_empty = True\n \n     def visit_Placeable(self, node):\n         if isinstance(node.expression, ast.Literal):\n             if node.expression.parse()[\"value\"]:\n-                self.is_empty = False\n+                self.is_pattern_empty = False\n         elif isinstance(node.expression, ast.SelectExpression):\n             self.generic_visit(node.expression)\n         else:\n-            self.is_empty = False\n+            self.is_pattern_empty = False\n \n     def visit_TextElement(self, node):\n         if node.value:\n-            self.is_empty = False\n+            self.is_pattern_empty = False\n \n \n def run_checks(entity, original, string):\n", "issue": "Warn on empty Fluent rich editor fields\n> In addition to the bug, I believe this is a UX regression? We should show a warning that one of the values is empty when trying to save such message.\r\n\r\nThe current practice is to consider any content in a Fluent value or attribute to qualify the whole message as not-empty. The check for this was added in #2675. Previously empty attributes may indeed have resulted in errors, due to stringifying as invalid Fluent.\r\n\r\n_Originally posted by @eemeli in https://github.com/mozilla/pontoon/issues/2973#issuecomment-1750344569_\r\n            \n", "before_files": [{"content": "import html\nimport re\n\nimport bleach\n\nfrom collections import defaultdict\nfrom fluent.syntax import FluentParser, ast\nfrom fluent.syntax.visitor import Visitor\n\nfrom pontoon.sync.formats.ftl import localizable_entries\n\n\nMAX_LENGTH_RE = re.compile(r\"MAX_LENGTH:( *)(\\d+)\", re.MULTILINE)\nparser = FluentParser()\n\n\ndef get_max_length(comment):\n    \"\"\"\n    Return max length value for an entity with MAX_LENTH.\n    \"\"\"\n    max_length = re.findall(MAX_LENGTH_RE, comment or \"\")\n\n    if max_length:\n        return int(max_length[0][1])\n\n    return None\n\n\nclass IsEmptyVisitor(Visitor):\n    def __init__(self):\n        self.is_empty = True\n\n    def visit_Placeable(self, node):\n        if isinstance(node.expression, ast.Literal):\n            if node.expression.parse()[\"value\"]:\n                self.is_empty = False\n        elif isinstance(node.expression, ast.SelectExpression):\n            self.generic_visit(node.expression)\n        else:\n            self.is_empty = False\n\n    def visit_TextElement(self, node):\n        if node.value:\n            self.is_empty = False\n\n\ndef run_checks(entity, original, string):\n    \"\"\"\n    Group all checks related to the base UI that get stored in the DB\n    :arg pontoon.base.models.Entity entity: Source entity\n    :arg basestring original: an original string\n    :arg basestring string: a translation\n    \"\"\"\n    checks = defaultdict(list)\n    resource_ext = entity.resource.format\n\n    if resource_ext == \"lang\":\n        # Newlines are not allowed in .lang files (bug 1190754)\n        if \"\\n\" in string:\n            checks[\"pErrors\"].append(\"Newline characters are not allowed\")\n\n        # Prevent translations exceeding the given length limit\n        max_length = get_max_length(entity.comment)\n\n        if max_length:\n            string_length = len(\n                html.unescape(bleach.clean(string, strip=True, tags=()))\n            )\n\n            if string_length > max_length:\n                checks[\"pErrors\"].append(\"Translation too long\")\n\n    # Bug 1599056: Original and translation must either both end in a newline,\n    # or none of them should.\n    if resource_ext == \"po\":\n        if original.endswith(\"\\n\") != string.endswith(\"\\n\"):\n            checks[\"pErrors\"].append(\"Ending newline mismatch\")\n\n    # Prevent empty translation submissions if not supported\n    if string == \"\" and not entity.resource.allows_empty_translations:\n        checks[\"pErrors\"].append(\"Empty translations are not allowed\")\n\n    # FTL checks\n    if resource_ext == \"ftl\" and string != \"\":\n        translation_ast = parser.parse_entry(string)\n        entity_ast = parser.parse_entry(entity.string)\n\n        # Parse error\n        if isinstance(translation_ast, ast.Junk):\n            checks[\"pErrors\"].append(translation_ast.annotations[0].message)\n\n        # Not a localizable entry\n        elif not isinstance(translation_ast, localizable_entries):\n            checks[\"pErrors\"].append(\n                \"Translation needs to be a valid localizable entry\"\n            )\n\n        # Message ID mismatch\n        elif entity_ast.id.name != translation_ast.id.name:\n            checks[\"pErrors\"].append(\"Translation key needs to match source string key\")\n\n        # Empty translation entry warning; set here rather than pontoon_non_db.py\n        # to avoid needing to parse the Fluent message twice.\n        else:\n            visitor = IsEmptyVisitor()\n            visitor.visit(translation_ast)\n            if visitor.is_empty:\n                checks[\"pndbWarnings\"].append(\"Empty translation\")\n\n    return checks\n", "path": "pontoon/checks/libraries/pontoon_db.py"}], "after_files": [{"content": "import html\nimport re\n\nimport bleach\n\nfrom collections import defaultdict\nfrom fluent.syntax import FluentParser, ast\nfrom fluent.syntax.visitor import Visitor\n\nfrom pontoon.sync.formats.ftl import localizable_entries\n\n\nMAX_LENGTH_RE = re.compile(r\"MAX_LENGTH:( *)(\\d+)\", re.MULTILINE)\nparser = FluentParser()\n\n\ndef get_max_length(comment):\n    \"\"\"\n    Return max length value for an entity with MAX_LENTH.\n    \"\"\"\n    max_length = re.findall(MAX_LENGTH_RE, comment or \"\")\n\n    if max_length:\n        return int(max_length[0][1])\n\n    return None\n\n\nclass IsEmptyVisitor(Visitor):\n    def __init__(self):\n        self.is_empty = False\n        self.is_pattern_empty = True\n\n    def visit_Pattern(self, node):\n        self.is_pattern_empty = True\n        self.visit(node.elements)\n        if self.is_pattern_empty:\n            self.is_empty = True\n\n    def visit_Placeable(self, node):\n        if isinstance(node.expression, ast.Literal):\n            if node.expression.parse()[\"value\"]:\n                self.is_pattern_empty = False\n        elif isinstance(node.expression, ast.SelectExpression):\n            self.generic_visit(node.expression)\n        else:\n            self.is_pattern_empty = False\n\n    def visit_TextElement(self, node):\n        if node.value:\n            self.is_pattern_empty = False\n\n\ndef run_checks(entity, original, string):\n    \"\"\"\n    Group all checks related to the base UI that get stored in the DB\n    :arg pontoon.base.models.Entity entity: Source entity\n    :arg basestring original: an original string\n    :arg basestring string: a translation\n    \"\"\"\n    checks = defaultdict(list)\n    resource_ext = entity.resource.format\n\n    if resource_ext == \"lang\":\n        # Newlines are not allowed in .lang files (bug 1190754)\n        if \"\\n\" in string:\n            checks[\"pErrors\"].append(\"Newline characters are not allowed\")\n\n        # Prevent translations exceeding the given length limit\n        max_length = get_max_length(entity.comment)\n\n        if max_length:\n            string_length = len(\n                html.unescape(bleach.clean(string, strip=True, tags=()))\n            )\n\n            if string_length > max_length:\n                checks[\"pErrors\"].append(\"Translation too long\")\n\n    # Bug 1599056: Original and translation must either both end in a newline,\n    # or none of them should.\n    if resource_ext == \"po\":\n        if original.endswith(\"\\n\") != string.endswith(\"\\n\"):\n            checks[\"pErrors\"].append(\"Ending newline mismatch\")\n\n    # Prevent empty translation submissions if not supported\n    if string == \"\" and not entity.resource.allows_empty_translations:\n        checks[\"pErrors\"].append(\"Empty translations are not allowed\")\n\n    # FTL checks\n    if resource_ext == \"ftl\" and string != \"\":\n        translation_ast = parser.parse_entry(string)\n        entity_ast = parser.parse_entry(entity.string)\n\n        # Parse error\n        if isinstance(translation_ast, ast.Junk):\n            checks[\"pErrors\"].append(translation_ast.annotations[0].message)\n\n        # Not a localizable entry\n        elif not isinstance(translation_ast, localizable_entries):\n            checks[\"pErrors\"].append(\n                \"Translation needs to be a valid localizable entry\"\n            )\n\n        # Message ID mismatch\n        elif entity_ast.id.name != translation_ast.id.name:\n            checks[\"pErrors\"].append(\"Translation key needs to match source string key\")\n\n        # Empty translation entry warning; set here rather than pontoon_non_db.py\n        # to avoid needing to parse the Fluent message twice.\n        else:\n            visitor = IsEmptyVisitor()\n            visitor.visit(translation_ast)\n            if visitor.is_empty:\n                checks[\"pndbWarnings\"].append(\"Empty translation\")\n\n    return checks\n", "path": "pontoon/checks/libraries/pontoon_db.py"}]}