Repository: mozilla/pontoon

Issue:
Fluent: Translation rejected by string imported with an extra line break
https://pontoon.mozilla.org/it/firefox-monitor-website/all-resources/?string=267702

I submitted a translation
https://github.com/mozilla/blurts-server/commit/0d87aa3f0ad876a2629ad2015687d4b8c35e7815

```
add-email-link = Aggiungi un indirizzo email
```

Then Pontoon proceeded to import the translation back with an extra line break, and rejecting the original translation.



Discussion:
This is a symptom of #2216, so essentially a dupe. I'll looking into it.

Note that fixing #2120 would also prevent this bug, but with the MessageFormat 2.0 transition, a fix for that bug is a little bit further away.

Resolving patch:
diff --git a/pontoon/base/templatetags/helpers.py b/pontoon/base/templatetags/helpers.py
--- a/pontoon/base/templatetags/helpers.py
+++ b/pontoon/base/templatetags/helpers.py
@@ -8,7 +8,7 @@
 from allauth.utils import get_request_param
 from bleach.linkifier import Linker
 from django_jinja import library
-from fluent.syntax import FluentParser, FluentSerializer, ast
+from fluent.syntax import FluentParser, ast
 from fluent.syntax.serializer import serialize_expression
 
 from django import template
@@ -22,7 +22,6 @@
 
 register = template.Library()
 parser = FluentParser()
-serializer = FluentSerializer()
 
 
 @library.global_function
@@ -396,7 +395,7 @@ def get_reconstructed_message(original, translation):
     else:
         content += ' { "" }'
 
-    return content
+    return parser.parse_entry(content)
 
 
 @library.filter
diff --git a/pontoon/pretranslation/pretranslate.py b/pontoon/pretranslation/pretranslate.py
--- a/pontoon/pretranslation/pretranslate.py
+++ b/pontoon/pretranslation/pretranslate.py
@@ -1,6 +1,8 @@
-from functools import reduce
 import operator
 
+from fluent.syntax import FluentSerializer
+from functools import reduce
+
 from django.db.models import CharField, Value as V
 from django.db.models.functions import Concat
 
@@ -17,6 +19,9 @@
 )
 
 
+serializer = FluentSerializer()
+
+
 def get_translations(entity, locale):
     """
     Get pretranslations for the entity-locale pair
@@ -54,7 +59,9 @@ def get_translations(entity, locale):
             translation = tm_response[0]["target"]
 
             if entity.string != entity_string:
-                translation = get_reconstructed_message(entity.string, translation)
+                translation = serializer.serialize_entry(
+                    get_reconstructed_message(entity.string, translation)
+                )
 
             strings = [(translation, None, tm_user)]
         else:


Test patch:
diff --git a/pontoon/base/tests/test_helpers.py b/pontoon/base/tests/test_helpers.py
--- a/pontoon/base/tests/test_helpers.py
+++ b/pontoon/base/tests/test_helpers.py
@@ -16,10 +16,12 @@
     get_reconstructed_message,
 )
 
-from fluent.syntax import FluentParser
+from fluent.syntax import FluentParser, FluentSerializer
+
 from pontoon.base.utils import aware_datetime
 
 parser = FluentParser()
+serializer = FluentSerializer()
 
 MULTILINE_SOURCE = """key =
     Simple String
@@ -188,7 +190,7 @@
             {
                 "original": "title = Marvel Cinematic Universe",
                 "translation": "Univers cinématographique Marvel",
-                "expected": "title = Univers cinématographique Marvel",
+                "expected": "title = Univers cinématographique Marvel\n",
             },
         ),
         (
@@ -196,7 +198,7 @@
             {
                 "original": "spoilers =\n    .who-dies = Who dies?",
                 "translation": "Qui meurt ?",
-                "expected": "spoilers =\n    .who-dies = Qui meurt ?",
+                "expected": "spoilers =\n    .who-dies = Qui meurt ?\n",
             },
         ),
         (
@@ -204,7 +206,7 @@
             {
                 "original": "time-travel = They discovered Time Travel",
                 "translation": "Ils ont inventé le\nvoyage temporel",
-                "expected": "time-travel =\n    Ils ont inventé le\n    voyage temporel",
+                "expected": "time-travel =\n    Ils ont inventé le\n    voyage temporel\n",
             },
         ),
         (
@@ -212,7 +214,7 @@
             {
                 "original": "slow-walks =\n    .title = They walk in slow motion",
                 "translation": "Ils se déplacent\nen mouvement lents",
-                "expected": "slow-walks =\n    .title =\n        Ils se déplacent\n        en mouvement lents",
+                "expected": "slow-walks =\n    .title =\n        Ils se déplacent\n        en mouvement lents\n",
             },
         ),
         (
@@ -220,7 +222,7 @@
             {
                 "original": "-my-term = MyTerm",
                 "translation": "Mon Terme",
-                "expected": "-my-term = Mon Terme",
+                "expected": "-my-term = Mon Terme\n",
             },
         ),
         (
@@ -228,7 +230,7 @@
             {
                 "original": "stark = Tony Stark\n    .hero = IronMan\n    .hair = black",
                 "translation": "Anthony Stark",
-                "expected": "stark = Anthony Stark",
+                "expected": "stark = Anthony Stark\n",
             },
         ),
         (
@@ -236,7 +238,7 @@
             {
                 "original": "with-term = I am { -term }",
                 "translation": "Je suis { -term }",
-                "expected": "with-term = Je suis { -term }",
+                "expected": "with-term = Je suis { -term }\n",
             },
         ),
     ]
@@ -334,4 +336,7 @@ def test_get_reconstructed_message(k):
         GET_RECONSTRUCTED_MESSAGE_TESTS[k]["original"],
         GET_RECONSTRUCTED_MESSAGE_TESTS[k]["translation"],
     )
-    assert result == GET_RECONSTRUCTED_MESSAGE_TESTS[k]["expected"]
+    assert (
+        serializer.serialize_entry(result)
+        == GET_RECONSTRUCTED_MESSAGE_TESTS[k]["expected"]
+    )