prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <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_
              
  
  </issue>
  <code>
  [start of pontoon/checks/libraries/pontoon_db.py]
  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 of pontoon/checks/libraries/pontoon_db.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: 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):
  
verification_info: {"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"}]}