prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Fluent pretranslations not serialized in canonical form
  Fluent pretranslations are not stored in a canonical form. For example, `key = {example}` should actually be stored as `key = { example }` (mind the whitespace).
  
  In the exported files in the repository, the strings appear as they should, which results in sync trying to import pretranslations, because they aren't the same as in Pontoon DB. That also triggers #2284. 
  
  In the UI (editor, string list) the strings are also rendered properly, which only makes the problem more difficult to spot.
  
  We should make sure that pretranslations are always stored them in the canonical form.
  
  --- 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/pretranslation/pretranslate.py`
  Content:
  ```
  1 import logging
  2 import operator
  3 import re
  4 
  5 from django.db.models import CharField, Value as V
  6 from django.db.models.functions import Concat
  7 
  8 from fluent.syntax import FluentParser, FluentSerializer
  9 from functools import reduce
  10 
  11 from pontoon.base.models import User, TranslatedResource
  12 from pontoon.machinery.utils import (
  13     get_google_translate_data,
  14     get_translation_memory_data,
  15 )
  16 from pontoon.pretranslation import AUTHORS
  17 from .transformer import ApplyPretranslation
  18 
  19 
  20 log = logging.getLogger(__name__)
  21 
  22 parser = FluentParser()
  23 serializer = FluentSerializer()
  24 
  25 
  26 def get_pretranslations(entity, locale, preserve_placeables=False):
  27     """
  28     Get pretranslations for the entity-locale pair using internal translation memory and
  29     Google's machine translation.
  30 
  31     For Fluent strings, uplift SelectExpressions, serialize Placeables as TextElements
  32     and then only pretranslate TextElements. Set the most frequent TextElement
  33     pretranslation author as the author of the entire pretranslation.
  34 
  35     :arg Entity entity: the Entity object
  36     :arg Locale locale: the Locale object
  37     :arg boolean preserve_placeables
  38 
  39     :returns: a list of tuples, consisting of:
  40         - a pretranslation of the entity
  41         - a plural form
  42         - a user (representing TM or GT service)
  43     """
  44     source = entity.string
  45     services = {k: User.objects.get(email=email) for k, email in AUTHORS.items()}
  46 
  47     if entity.resource.format == "ftl":
  48         entry = parser.parse_entry(source)
  49         pretranslate = ApplyPretranslation(
  50             locale, entry, get_pretranslated_data, preserve_placeables
  51         )
  52 
  53         try:
  54             pretranslate.visit(entry)
  55         except ValueError as e:
  56             log.info(f"Fluent pretranslation error: {e}")
  57             return []
  58 
  59         pretranslation = serializer.serialize_entry(entry)
  60 
  61         authors = [services[service] for service in pretranslate.services]
  62         author = max(set(authors), key=authors.count) if authors else services["tm"]
  63 
  64         return [(pretranslation, None, author)]
  65 
  66     else:
  67         pretranslation, service = get_pretranslated_data(
  68             source, locale, preserve_placeables
  69         )
  70 
  71         if pretranslation is None:
  72             return []
  73 
  74         author = services[service]
  75         if entity.string_plural == "":
  76             return [(pretranslation, None, author)]
  77         else:
  78             plural_forms = range(0, locale.nplurals or 1)
  79             return [
  80                 (pretranslation, plural_form, author) for plural_form in plural_forms
  81             ]
  82 
  83 
  84 def get_pretranslated_data(source, locale, preserve_placeables):
  85     # Empty strings do not need translation
  86     if re.search("^\\s*$", source):
  87         return source, "tm"
  88 
  89     # Try to get matches from Translation Memory
  90     tm_response = get_translation_memory_data(text=source, locale=locale)
  91     tm_perfect = [t for t in tm_response if int(t["quality"]) == 100]
  92     if tm_perfect:
  93         return tm_perfect[0]["target"], "tm"
  94 
  95     # Fetch from Google Translate
  96     elif locale.google_translate_code:
  97         gt_response = get_google_translate_data(
  98             text=source, locale=locale, preserve_placeables=preserve_placeables
  99         )
  100         if gt_response["status"]:
  101             return gt_response["translation"], "gt"
  102 
  103     return None, None
  104 
  105 
  106 def update_changed_instances(tr_filter, tr_dict, translations):
  107     """
  108     Update the latest activity and stats for changed Locales, ProjectLocales
  109     & TranslatedResources
  110     """
  111     tr_filter = tuple(tr_filter)
  112     # Combine all generated filters with an OK operator.
  113     # `operator.ior` is the '|' Python operator, which turns into a logical OR
  114     # when used between django ORM query objects.
  115     tr_query = reduce(operator.ior, tr_filter)
  116 
  117     translatedresources = TranslatedResource.objects.filter(tr_query).annotate(
  118         locale_resource=Concat(
  119             "locale_id", V("-"), "resource_id", output_field=CharField()
  120         )
  121     )
  122 
  123     translatedresources.update_stats()
  124 
  125     for tr in translatedresources:
  126         index = tr_dict[tr.locale_resource]
  127         translation = translations[index]
  128         translation.update_latest_translation()
  129
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/pontoon/pretranslation/pretranslate.py b/pontoon/pretranslation/pretranslate.py
  --- a/pontoon/pretranslation/pretranslate.py
  +++ b/pontoon/pretranslation/pretranslate.py
  @@ -58,6 +58,10 @@
   
           pretranslation = serializer.serialize_entry(entry)
   
  +        # Parse and serialize pretranslation again in order to assure cannonical style
  +        parsed_pretranslation = parser.parse_entry(pretranslation)
  +        pretranslation = serializer.serialize_entry(parsed_pretranslation)
  +
           authors = [services[service] for service in pretranslate.services]
           author = max(set(authors), key=authors.count) if authors else services["tm"]
   
  @@ -82,7 +86,7 @@
   
   
   def get_pretranslated_data(source, locale, preserve_placeables):
  -    # Empty strings do not need translation
  +    # Empty strings and strings containing whitespace only do not need translation
       if re.search("^\\s*$", source):
           return source, "tm"
  
verification_info: {"golden_diff": "diff --git a/pontoon/pretranslation/pretranslate.py b/pontoon/pretranslation/pretranslate.py\n--- a/pontoon/pretranslation/pretranslate.py\n+++ b/pontoon/pretranslation/pretranslate.py\n@@ -58,6 +58,10 @@\n \n         pretranslation = serializer.serialize_entry(entry)\n \n+        # Parse and serialize pretranslation again in order to assure cannonical style\n+        parsed_pretranslation = parser.parse_entry(pretranslation)\n+        pretranslation = serializer.serialize_entry(parsed_pretranslation)\n+\n         authors = [services[service] for service in pretranslate.services]\n         author = max(set(authors), key=authors.count) if authors else services[\"tm\"]\n \n@@ -82,7 +86,7 @@\n \n \n def get_pretranslated_data(source, locale, preserve_placeables):\n-    # Empty strings do not need translation\n+    # Empty strings and strings containing whitespace only do not need translation\n     if re.search(\"^\\\\s*$\", source):\n         return source, \"tm\"\n", "issue": "Fluent pretranslations not serialized in canonical form\nFluent pretranslations are not stored in a canonical form. For example, `key = {example}` should actually be stored as `key = { example }` (mind the whitespace).\r\n\r\nIn the exported files in the repository, the strings appear as they should, which results in sync trying to import pretranslations, because they aren't the same as in Pontoon DB. That also triggers #2284. \r\n\r\nIn the UI (editor, string list) the strings are also rendered properly, which only makes the problem more difficult to spot.\r\n\r\nWe should make sure that pretranslations are always stored them in the canonical form.\n", "before_files": [{"content": "import logging\nimport operator\nimport re\n\nfrom django.db.models import CharField, Value as V\nfrom django.db.models.functions import Concat\n\nfrom fluent.syntax import FluentParser, FluentSerializer\nfrom functools import reduce\n\nfrom pontoon.base.models import User, TranslatedResource\nfrom pontoon.machinery.utils import (\n    get_google_translate_data,\n    get_translation_memory_data,\n)\nfrom pontoon.pretranslation import AUTHORS\nfrom .transformer import ApplyPretranslation\n\n\nlog = logging.getLogger(__name__)\n\nparser = FluentParser()\nserializer = FluentSerializer()\n\n\ndef get_pretranslations(entity, locale, preserve_placeables=False):\n    \"\"\"\n    Get pretranslations for the entity-locale pair using internal translation memory and\n    Google's machine translation.\n\n    For Fluent strings, uplift SelectExpressions, serialize Placeables as TextElements\n    and then only pretranslate TextElements. Set the most frequent TextElement\n    pretranslation author as the author of the entire pretranslation.\n\n    :arg Entity entity: the Entity object\n    :arg Locale locale: the Locale object\n    :arg boolean preserve_placeables\n\n    :returns: a list of tuples, consisting of:\n        - a pretranslation of the entity\n        - a plural form\n        - a user (representing TM or GT service)\n    \"\"\"\n    source = entity.string\n    services = {k: User.objects.get(email=email) for k, email in AUTHORS.items()}\n\n    if entity.resource.format == \"ftl\":\n        entry = parser.parse_entry(source)\n        pretranslate = ApplyPretranslation(\n            locale, entry, get_pretranslated_data, preserve_placeables\n        )\n\n        try:\n            pretranslate.visit(entry)\n        except ValueError as e:\n            log.info(f\"Fluent pretranslation error: {e}\")\n            return []\n\n        pretranslation = serializer.serialize_entry(entry)\n\n        authors = [services[service] for service in pretranslate.services]\n        author = max(set(authors), key=authors.count) if authors else services[\"tm\"]\n\n        return [(pretranslation, None, author)]\n\n    else:\n        pretranslation, service = get_pretranslated_data(\n            source, locale, preserve_placeables\n        )\n\n        if pretranslation is None:\n            return []\n\n        author = services[service]\n        if entity.string_plural == \"\":\n            return [(pretranslation, None, author)]\n        else:\n            plural_forms = range(0, locale.nplurals or 1)\n            return [\n                (pretranslation, plural_form, author) for plural_form in plural_forms\n            ]\n\n\ndef get_pretranslated_data(source, locale, preserve_placeables):\n    # Empty strings do not need translation\n    if re.search(\"^\\\\s*$\", source):\n        return source, \"tm\"\n\n    # Try to get matches from Translation Memory\n    tm_response = get_translation_memory_data(text=source, locale=locale)\n    tm_perfect = [t for t in tm_response if int(t[\"quality\"]) == 100]\n    if tm_perfect:\n        return tm_perfect[0][\"target\"], \"tm\"\n\n    # Fetch from Google Translate\n    elif locale.google_translate_code:\n        gt_response = get_google_translate_data(\n            text=source, locale=locale, preserve_placeables=preserve_placeables\n        )\n        if gt_response[\"status\"]:\n            return gt_response[\"translation\"], \"gt\"\n\n    return None, None\n\n\ndef update_changed_instances(tr_filter, tr_dict, translations):\n    \"\"\"\n    Update the latest activity and stats for changed Locales, ProjectLocales\n    & TranslatedResources\n    \"\"\"\n    tr_filter = tuple(tr_filter)\n    # Combine all generated filters with an OK operator.\n    # `operator.ior` is the '|' Python operator, which turns into a logical OR\n    # when used between django ORM query objects.\n    tr_query = reduce(operator.ior, tr_filter)\n\n    translatedresources = TranslatedResource.objects.filter(tr_query).annotate(\n        locale_resource=Concat(\n            \"locale_id\", V(\"-\"), \"resource_id\", output_field=CharField()\n        )\n    )\n\n    translatedresources.update_stats()\n\n    for tr in translatedresources:\n        index = tr_dict[tr.locale_resource]\n        translation = translations[index]\n        translation.update_latest_translation()\n", "path": "pontoon/pretranslation/pretranslate.py"}], "after_files": [{"content": "import logging\nimport operator\nimport re\n\nfrom django.db.models import CharField, Value as V\nfrom django.db.models.functions import Concat\n\nfrom fluent.syntax import FluentParser, FluentSerializer\nfrom functools import reduce\n\nfrom pontoon.base.models import User, TranslatedResource\nfrom pontoon.machinery.utils import (\n    get_google_translate_data,\n    get_translation_memory_data,\n)\nfrom pontoon.pretranslation import AUTHORS\nfrom .transformer import ApplyPretranslation\n\n\nlog = logging.getLogger(__name__)\n\nparser = FluentParser()\nserializer = FluentSerializer()\n\n\ndef get_pretranslations(entity, locale, preserve_placeables=False):\n    \"\"\"\n    Get pretranslations for the entity-locale pair using internal translation memory and\n    Google's machine translation.\n\n    For Fluent strings, uplift SelectExpressions, serialize Placeables as TextElements\n    and then only pretranslate TextElements. Set the most frequent TextElement\n    pretranslation author as the author of the entire pretranslation.\n\n    :arg Entity entity: the Entity object\n    :arg Locale locale: the Locale object\n    :arg boolean preserve_placeables\n\n    :returns: a list of tuples, consisting of:\n        - a pretranslation of the entity\n        - a plural form\n        - a user (representing TM or GT service)\n    \"\"\"\n    source = entity.string\n    services = {k: User.objects.get(email=email) for k, email in AUTHORS.items()}\n\n    if entity.resource.format == \"ftl\":\n        entry = parser.parse_entry(source)\n        pretranslate = ApplyPretranslation(\n            locale, entry, get_pretranslated_data, preserve_placeables\n        )\n\n        try:\n            pretranslate.visit(entry)\n        except ValueError as e:\n            log.info(f\"Fluent pretranslation error: {e}\")\n            return []\n\n        pretranslation = serializer.serialize_entry(entry)\n\n        # Parse and serialize pretranslation again in order to assure cannonical style\n        parsed_pretranslation = parser.parse_entry(pretranslation)\n        pretranslation = serializer.serialize_entry(parsed_pretranslation)\n\n        authors = [services[service] for service in pretranslate.services]\n        author = max(set(authors), key=authors.count) if authors else services[\"tm\"]\n\n        return [(pretranslation, None, author)]\n\n    else:\n        pretranslation, service = get_pretranslated_data(\n            source, locale, preserve_placeables\n        )\n\n        if pretranslation is None:\n            return []\n\n        author = services[service]\n        if entity.string_plural == \"\":\n            return [(pretranslation, None, author)]\n        else:\n            plural_forms = range(0, locale.nplurals or 1)\n            return [\n                (pretranslation, plural_form, author) for plural_form in plural_forms\n            ]\n\n\ndef get_pretranslated_data(source, locale, preserve_placeables):\n    # Empty strings and strings containing whitespace only do not need translation\n    if re.search(\"^\\\\s*$\", source):\n        return source, \"tm\"\n\n    # Try to get matches from Translation Memory\n    tm_response = get_translation_memory_data(text=source, locale=locale)\n    tm_perfect = [t for t in tm_response if int(t[\"quality\"]) == 100]\n    if tm_perfect:\n        return tm_perfect[0][\"target\"], \"tm\"\n\n    # Fetch from Google Translate\n    elif locale.google_translate_code:\n        gt_response = get_google_translate_data(\n            text=source, locale=locale, preserve_placeables=preserve_placeables\n        )\n        if gt_response[\"status\"]:\n            return gt_response[\"translation\"], \"gt\"\n\n    return None, None\n\n\ndef update_changed_instances(tr_filter, tr_dict, translations):\n    \"\"\"\n    Update the latest activity and stats for changed Locales, ProjectLocales\n    & TranslatedResources\n    \"\"\"\n    tr_filter = tuple(tr_filter)\n    # Combine all generated filters with an OK operator.\n    # `operator.ior` is the '|' Python operator, which turns into a logical OR\n    # when used between django ORM query objects.\n    tr_query = reduce(operator.ior, tr_filter)\n\n    translatedresources = TranslatedResource.objects.filter(tr_query).annotate(\n        locale_resource=Concat(\n            \"locale_id\", V(\"-\"), \"resource_id\", output_field=CharField()\n        )\n    )\n\n    translatedresources.update_stats()\n\n    for tr in translatedresources:\n        index = tr_dict[tr.locale_resource]\n        translation = translations[index]\n        translation.update_latest_translation()\n", "path": "pontoon/pretranslation/pretranslate.py"}]}