Repository: WeblateOrg/weblate

Issue:
Gettext line wrapping broken immediately after merging a PR made by weblate
### Describe the issue

I just merged a PR from weblate https://github.com/Revolutionary-Games/Thrive/pull/4778 (I had checked on weblate beforehand that there were 0 pending changes to be committed) and weblate decided to immediately open a new PR that again has broken gettext line changes. This seems kind of a new problem after this other issue I had with the line wrapping was fixed: https://github.com/WeblateOrg/weblate/issues/10392 (or maybe I just didn't notice this before due to that more visible issue). Edit: could this be caused by the translation cleanup plugin, which presumably runs immediately when new remote code hook is detected by weblate?

Edit: forgot to show the PR that weblate immediately opened: https://github.com/Revolutionary-Games/Thrive/pull/4798

### I already tried

- [X] I've read and searched [the documentation](https://docs.weblate.org/).
- [X] I've searched for similar filed issues in this repository.

### Steps to reproduce the behavior

1. Check on weblate admin UI that all changes of a project are committed (though I didn't cause a new push, the weblate PR was not up to date with master at the time of merging)
2. Merge the PR from weblate
3. Weblate immediately makes a new PR where it messes up 2 of the 3 changed languages line changes

### Expected behavior

As always, I expect the gettext line wrapping customization plugin to always work.

### Screenshots

_No response_

### Exception traceback

_No response_

### How do you run Weblate?

PyPI module

### Weblate versions

 * Weblate: 5.3.1
 * Django: 4.2.9
 * siphashc: 2.3
 * translate-toolkit: 3.11.1
 * lxml: 4.9.4
 * Pillow: 10.1.0
 * nh3: 0.2.15
 * python-dateutil: 2.8.2
 * social-auth-core: 4.5.1
 * social-auth-app-django: 5.4.0
 * django-crispy-forms: 2.1
 * oauthlib: 3.2.2
 * django-compressor: 4.4
 * djangorestframework: 3.14.0
 * django-filter: 23.5
 * django-appconf: 1.0.6
 * user-agents: 2.2.0
 * filelock: 3.13.1
 * rapidfuzz: 3.5.2
 * openpyxl: 3.1.2
 * celery: 5.3.6
 * django-celery-beat: 2.5.0
 * kombu: 5.3.4
 * translation-finder: 2.16
 * weblate-language-data: 2023.9
 * html2text: 2020.1.16
 * pycairo: 1.25.1
 * PyGObject: 3.46.0
 * diff-match-patch: 20230430
 * requests: 2.31.0
 * django-redis: 5.4.0
 * hiredis: 2.2.3
 * sentry-sdk: 1.39.1
 * Cython: 3.0.7
 * misaka: 2.1.1
 * GitPython: 3.1.40
 * borgbackup: 1.2.7
 * pyparsing: 3.1.1
 * ahocorasick_rs: 0.20.0
 * python-redis-lock: 4.0.0
 * charset-normalizer: 3.3.2
 * Python: 3.12.1
 * Git: 2.43.0
 * psycopg: 3.1.16
 * psycopg-binary: 3.1.16
 * phply: 1.2.6
 * ruamel.yaml: 0.18.5
 * tesserocr: 2.6.2
 * boto3: 1.34.11
 * aeidon: 1.13
 * iniparse: 0.5
 * Mercurial: 6.6.1
 * git-svn: 2.43.0
 * PostgreSQL server: 15.4
 * Database backends: django.db.backends.postgresql
 * PostgreSQL implementation: psycopg3 (binary)
 * Cache backends: default:RedisCache, avatar:FileBasedCache
 * Email setup: django.core.mail.backends.smtp.EmailBackend: mail.revolutionarygamesstudio.com
 * OS encoding: filesystem=utf-8, default=utf-8
 * Celery: redis://localhost:6379, redis://localhost:6379, regular
 * Platform: Linux 6.6.8-200.fc39.x86_64 (x86_64)


### Weblate deploy checks

```shell
System check identified some issues:

WARNINGS:
?: (weblate.W033.Gerrit) Failure in loading VCS module for Gerrit: Traceback (most recent call last):
  File "/home/weblate/weblate-env/bin/git-review", line 5, in <module>
    from git_review.cmd import main
  File "/home/weblate/weblate-env/lib/python3.12/site-packages/git_review/cmd.py", line 34, in <module>
    import pkg_resources
ModuleNotFoundError: No module named 'pkg_resources'
 (1)
        HINT: https://docs.weblate.org/en/weblate-5.3.1/vcs.html

INFOS:
?: (weblate.I021) Error collection is not set up, it is highly recommended for production use
        HINT: https://docs.weblate.org/en/weblate-5.3.1/admin/install.html#collecting-errors

System check identified 2 issues (1 silenced).
```


### Additional context

This might be caused by the gettext cleanup plugin I installed as suggested for the issue I was having with old translations sticking around. I just tried reinstalling the gettext line wrap plugin in case it somehow was again broken (I'll know in about a week if that did anything or not).


Discussion:
https://github.com/WeblateOrg/weblate/issues/10392 was fixed in translate-toolkit 3.12 which you don't have installed, can you try upgrading it?
So Weblate doesn't require the newest translate-toolkit version, but I should be able to upgrade that pip package separately? I'll try that.

Edit: pip says that Weblate doesn't like that new translate toolkit, but I guess it is safe to try anyway?
Yes, that should be safe for 3.12. I've adjusted the milestone of https://github.com/WeblateOrg/weblate/issues/10392 to correctly show where the fix will actually land (we had to shuffle the release schedule a bit and some Weblate was released earlier than translate-toolkit).

PS: Can you please grant me admin permissions on https://translate.revolutionarygamesstudio.com/projects/thrive/ so that I can check (and possibly replicate) project configuration? I'm registered as `nijel` there as well.
Looking at https://github.com/Revolutionary-Games/Thrive/pull/4798 it seems that the lack of wrapping is coming from the `msgmerge` add-on. I've done some cleanup in related code today in #10765 and #10766, but I don't think these are going to address this issue.
Alright I ran `pip install -U translate-toolkit` to get:
```
Installing collected packages: translate-toolkit
  Attempting uninstall: translate-toolkit
    Found existing installation: translate-toolkit 3.11.1
    Uninstalling translate-toolkit-3.11.1:
      Successfully uninstalled translate-toolkit-3.11.1
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
weblate 5.3.1 requires translate-toolkit<3.12,>=3.11.0, but you have translate-toolkit 3.12.1 which is incompatible.
Successfully installed translate-toolkit-3.12.1
```

And restarted Weblate. I think I maybe managed to do the permissions setup right, it was a bit of a guess what I should pick on the site as it is shown in Finnish for me.
Is there something else besides Weblate code touching the Weblate repository? For example, there could be some Git repository hooks installed. Looking at the changes, there are ones which I cannot explain by Weblate configuration you have.

An example is location change in the translation files:

![image](https://github.com/WeblateOrg/weblate/assets/212189/fb003c03-a6ad-4864-a01c-f5e83772b7af)

* Weblate doesn't update these automatically.
* The `msgmerge` add-on could do that, but you don't have installed. 
* The other option would be that the cleanup add-on mistakenly removed the string and Weblate added it back, but it would lose the translation during this, what didn't happen.
* User replaced the file via upload, but that should be visible in the history.

So, eventually, I'm pretty sure this comes from `msgmerge` being invoked on the file without `--no-wrap`. The only question is who does that and why we can't see that in the history.
I mean there shouldn't be anything intentionally running on the server that modifies the weblate local repository. I did try to clean diverged history by deleting and recreating the repository months ago, but I think I maybe undid that as that broke way more things than it fixed (I made a folder backup that I think I restored). 

I haven't done any configuration or anything like that manually on the weblate git repository. I'm not fully sure how to check if there's any hooks or anything like that but I see only `.sample` files in the `/home/weblate/weblate/data/vcs/thrive/thrive-game/.git/hooks` folder so there perhaps aren't any hooks.

Resolving patch:
diff --git a/weblate/trans/models/component.py b/weblate/trans/models/component.py
--- a/weblate/trans/models/component.py
+++ b/weblate/trans/models/component.py
@@ -1968,7 +1968,7 @@ def reuse_self(translation):
 
         # Fire postponed post commit signals
         for component in components.values():
-            vcs_post_commit.send(sender=self.__class__, component=component)
+            component.send_post_commit_signal()
             component.store_local_revision()
             component.update_import_alerts(delete=False)
 
@@ -2021,7 +2021,7 @@ def commit_files(
 
             # Send post commit signal
             if signals:
-                vcs_post_commit.send(sender=self.__class__, component=self)
+                self.send_post_commit_signal()
 
             self.store_local_revision()
 
@@ -2031,6 +2031,9 @@ def commit_files(
 
             return True
 
+    def send_post_commit_signal(self):
+        vcs_post_commit.send(sender=self.__class__, component=self)
+
     def get_parse_error_message(self, error) -> str:
         error_message = getattr(error, "strerror", "")
         if not error_message:
diff --git a/weblate/trans/models/translation.py b/weblate/trans/models/translation.py
--- a/weblate/trans/models/translation.py
+++ b/weblate/trans/models/translation.py
@@ -1088,6 +1088,7 @@ def handle_source(self, request, fileobj):
                 files=filenames,
                 author=request.user.get_author_name(),
                 extra_context={"addon_name": "Source update"},
+                signals=False,
             ):
                 self.handle_store_change(
                     request,
@@ -1095,6 +1096,9 @@ def handle_source(self, request, fileobj):
                     previous_revision,
                     change=Change.ACTION_REPLACE_UPLOAD,
                 )
+                # Emit signals later to avoid cleanup add-on to store translation
+                # revision before parsing
+                component.send_post_commit_signal()
         return (0, 0, self.unit_set.count(), self.unit_set.count())
 
     def handle_replace(self, request, fileobj):
@@ -1125,7 +1129,10 @@ def handle_replace(self, request, fileobj):
             # Commit to VCS
             previous_revision = self.component.repository.last_revision
             if self.git_commit(
-                request.user, request.user.get_author_name(), store_hash=False
+                request.user,
+                request.user.get_author_name(),
+                store_hash=False,
+                signals=False,
             ):
                 # Drop store cache
                 self.handle_store_change(
@@ -1134,6 +1141,9 @@ def handle_replace(self, request, fileobj):
                     previous_revision,
                     change=Change.ACTION_REPLACE_UPLOAD,
                 )
+                # Emit signals later to avoid cleanup add-on to store translation
+                # revision before parsing
+                self.component.send_post_commit_signal()
 
         return (0, 0, self.unit_set.count(), len(store2.content_units))
 


Test patch:
diff --git a/weblate/trans/tests/test_files.py b/weblate/trans/tests/test_files.py
--- a/weblate/trans/tests/test_files.py
+++ b/weblate/trans/tests/test_files.py
@@ -13,7 +13,7 @@
 
 from weblate.formats.helpers import NamedBytesIO
 from weblate.trans.forms import SimpleUploadForm
-from weblate.trans.models import ComponentList
+from weblate.trans.models import Change, ComponentList
 from weblate.trans.tests.test_views import ViewTestCase
 from weblate.trans.tests.utils import get_test_file
 
@@ -353,10 +353,14 @@ def test_import(self):
         self.assertEqual(translation.stats.all, 4)
 
     def test_replace(self):
+        translation = self.get_translation()
+        self.assertFalse(
+            translation.change_set.filter(action=Change.ACTION_REPLACE_UPLOAD).exists()
+        )
         self.user.is_superuser = True
         self.user.save()
         with open(TEST_ANDROID, "rb") as handle:
-            self.client.post(
+            response = self.client.post(
                 reverse(
                     "upload",
                     kwargs={"path": self.component.source_translation.get_url_path()},
@@ -367,12 +371,18 @@ def test_replace(self):
                     "author_name": self.user.full_name,
                     "author_email": self.user.email,
                 },
+                follow=True,
             )
+            messages = list(response.context["messages"])
+            self.assertIn("updated: 2", messages[0].message)
         # Verify stats
         translation = self.get_translation()
         self.assertEqual(translation.stats.translated, 0)
         self.assertEqual(translation.stats.fuzzy, 0)
         self.assertEqual(translation.stats.all, 2)
+        self.assertTrue(
+            translation.change_set.filter(action=Change.ACTION_REPLACE_UPLOAD).exists()
+        )
 
 
 class CSVImportTest(ViewTestCase):
@@ -561,6 +571,7 @@ class ImportSourceTest(ImportBaseTest):
     test_file = TEST_POT_CHARSET
     expected = "Processed 3 strings from the uploaded files"
     expected_count = 3
+    expected_uploads = 1
 
     def setUp(self):
         super().setUp()
@@ -568,6 +579,10 @@ def setUp(self):
 
     def test_import(self):
         """Test importing normally."""
+        translation = self.get_translation()
+        self.assertFalse(
+            translation.change_set.filter(action=Change.ACTION_REPLACE_UPLOAD).exists()
+        )
         response = self.do_import(method="source", follow=True)
         self.assertRedirects(response, self.translation.get_absolute_url())
         messages = list(response.context["messages"])
@@ -583,6 +598,11 @@ def test_import(self):
         unit = self.get_unit()
         self.assertEqual(unit.target, "")
 
+        self.assertEqual(
+            translation.change_set.filter(action=Change.ACTION_REPLACE_UPLOAD).count(),
+            self.expected_uploads,
+        )
+
 
 class ImportAddTest(ImportBaseTest):
     """Testing of source strings update imports."""
@@ -626,6 +646,7 @@ class ImportSourceBrokenTest(ImportSourceTest):
     test_file = TEST_POT
     expected = 'Charset "CHARSET" is not a portable encoding name.'
     expected_count = 4
+    expected_uploads = 0
 
 
 class DownloadMultiTest(ViewTestCase):