Skip to content

feat(file_format_params): Gettext PO header configuration with file format parameters#18982

Open
gersona wants to merge 10 commits intoWeblateOrg:mainfrom
gersona:16971_gettex_settings_as_file_format_params
Open

feat(file_format_params): Gettext PO header configuration with file format parameters#18982
gersona wants to merge 10 commits intoWeblateOrg:mainfrom
gersona:16971_gettex_settings_as_file_format_params

Conversation

@gersona
Copy link
Copy Markdown
Contributor

@gersona gersona commented Apr 13, 2026

@gersona
Copy link
Copy Markdown
Contributor Author

gersona commented Apr 13, 2026

Should the gettext file format parameters also apply to the headers set in the PO exporters? For example, weblate/formats/exporters.py (lines 71–78) always sets x_generator, language_team, etc., without consulting file_format_params.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

❌ 16 Tests Failed:

Tests completed Failed Passed Skipped
6916 16 6900 637
View the top 3 failed test(s) by shortest run time
weblate.billing.tests.BillingTest::test_expiry
Stack Traces | 0.282s run time
self = <weblate.billing.tests.BillingTest testMethod=test_expiry>

    @override_settings(EMAIL_SUBJECT_PREFIX="")
    def test_expiry(self) -> None:
        self.add_project()
    
        # Paid
        schedule_removal()
        notify_expired()
        perform_removal()
        self.assertEqual(len(mail.outbox), 0)
        self.refresh_from_db()
        self.assertIsNone(self.billing.removal)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.state, Billing.STATE_ACTIVE)
        self.assertEqual(self.billing.projects.count(), 1)
    
        # Not paid
        self.invoice.start -= timedelta(days=14)
        self.invoice.end -= timedelta(days=14)
        self.invoice.save()
        schedule_removal()
        notify_expired()
        perform_removal()
        self.assertEqual(len(mail.outbox), 1)
        self.refresh_from_db()
        self.assertIsNone(self.billing.removal)
        self.assertEqual(self.billing.state, Billing.STATE_ACTIVE)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertEqual(mail.outbox.pop().subject, "Your billing plan has expired")
    
        # Not paid for long
        self.invoice.start -= timedelta(days=30)
        self.invoice.end -= timedelta(days=30)
        self.invoice.save()
        schedule_removal()
        notify_expired()
        perform_removal()
        self.assertEqual(len(mail.outbox), 1)
        self.refresh_from_db()
        self.assertIsNotNone(self.billing.removal)
        self.assertEqual(self.billing.state, Billing.STATE_ACTIVE)
        self.assertFalse(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertEqual(
            mail.outbox.pop().subject,
            "Your translation project is scheduled for removal",
        )
    
        # Final removal
        self.billing.removal = timezone.now() - timedelta(days=30)
        self.billing.save(skip_limits=True)
>       perform_removal()

weblate/billing/tests.py:264: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/billing/tasks.py:146: in perform_removal
    remove_single_billing.delay(bill.pk)
.venv/lib/python3.14.../celery/app/task.py:463: in delay
    return self.apply_async(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:624: in apply_async
    return self.apply(args, kwargs, task_id=task_id or uuid(),
.venv/lib/python3.14.../celery/app/task.py:862: in apply
    ret = tracer(task_id, args, kwargs, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:605: in trace_task
    I, R, state, retval = on_error(task_request, exc)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:585: in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/billing/tasks.py:136: in remove_single_billing
    project_removal(prj.id, None)
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:606: in project_removal
    create_project_backup(pk)
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:861: in create_project_backup
    backup.backup_project(project, user)
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f545281bd90>
obj = <Project: test0>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.billing.tests.BillingTest::test_free_trial
Stack Traces | 0.338s run time
self = <weblate.billing.tests.BillingTest testMethod=test_free_trial>

    def test_free_trial(self) -> None:
        self.plan.price = 0
        self.plan.yearly_price = 0
        self.plan.save()
>       self.test_trial()

weblate/billing/tests.py:372: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14.../django/test/utils.py:458: in inner
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
weblate/billing/tests.py:358: in test_trial
    perform_removal()
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/billing/tasks.py:146: in perform_removal
    remove_single_billing.delay(bill.pk)
.venv/lib/python3.14.../celery/app/task.py:463: in delay
    return self.apply_async(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:624: in apply_async
    return self.apply(args, kwargs, task_id=task_id or uuid(),
.venv/lib/python3.14.../celery/app/task.py:862: in apply
    ret = tracer(task_id, args, kwargs, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:605: in trace_task
    I, R, state, retval = on_error(task_request, exc)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:585: in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/billing/tasks.py:136: in remove_single_billing
    project_removal(prj.id, None)
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:606: in project_removal
    create_project_backup(pk)
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:861: in create_project_backup
    backup.backup_project(project, user)
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f5453918910>
obj = <Project: test0>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.billing.tests.BillingTest::test_trial
Stack Traces | 0.737s run time
self = <weblate.billing.tests.BillingTest testMethod=test_trial>

    @override_settings(EMAIL_SUBJECT_PREFIX="")
    def test_trial(self) -> None:
        self.billing.state = Billing.STATE_TRIAL
        self.billing.save(skip_limits=True)
        self.billing.invoice_set.all().delete()
        self.add_project()
    
        # No expiry set
        billing_check()
        notify_expired()
        perform_removal()
        self.refresh_from_db()
        self.assertEqual(self.billing.state, Billing.STATE_TRIAL)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertIsNone(self.billing.removal)
        self.assertEqual(len(mail.outbox), 0)
    
        # Future expiry
        self.billing.expiry = timezone.now() + timedelta(days=30)
        self.billing.save(skip_limits=True)
        billing_check()
        notify_expired()
        perform_removal()
        self.refresh_from_db()
        self.assertEqual(self.billing.state, Billing.STATE_TRIAL)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertIsNone(self.billing.removal)
        self.assertEqual(len(mail.outbox), 0)
    
        # Close expiry
        self.billing.expiry = timezone.now() + timedelta(days=1)
        self.billing.save(skip_limits=True)
        billing_check()
        notify_expired()
        perform_removal()
        self.refresh_from_db()
        self.assertEqual(self.billing.state, Billing.STATE_TRIAL)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertIsNone(self.billing.removal)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(
            mail.outbox.pop().subject, "Your trial period is about to expire"
        )
    
        # Past expiry
        self.billing.expiry = timezone.now() - timedelta(days=1)
        self.billing.save(skip_limits=True)
        billing_check()
        notify_expired()
        perform_removal()
        self.refresh_from_db()
        self.assertEqual(self.billing.state, Billing.STATE_TRIAL)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertIsNone(self.billing.expiry)
        self.assertIsNotNone(self.billing.removal)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(
            mail.outbox.pop().subject,
            "Your translation project is scheduled for removal",
        )
    
        # There should be notification sent when removal is scheduled
        billing_check()
        notify_expired()
        perform_removal()
        self.refresh_from_db()
        self.assertEqual(self.billing.state, Billing.STATE_TRIAL)
        self.assertTrue(self.billing.paid)
        self.assertEqual(self.billing.projects.count(), 1)
        self.assertIsNotNone(self.billing.removal)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(
            mail.outbox.pop().subject,
            "Your translation project is scheduled for removal",
        )
    
        # Removal
        self.billing.removal = timezone.now() - timedelta(days=1)
        self.billing.save(skip_limits=True)
        billing_check()
>       perform_removal()

weblate/billing/tests.py:358: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/billing/tasks.py:146: in perform_removal
    remove_single_billing.delay(bill.pk)
.venv/lib/python3.14.../celery/app/task.py:463: in delay
    return self.apply_async(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:624: in apply_async
    return self.apply(args, kwargs, task_id=task_id or uuid(),
.venv/lib/python3.14.../celery/app/task.py:862: in apply
    ret = tracer(task_id, args, kwargs, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:605: in trace_task
    I, R, state, retval = on_error(task_request, exc)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:585: in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/billing/tasks.py:136: in remove_single_billing
    project_removal(prj.id, None)
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:606: in project_removal
    create_project_backup(pk)
.venv/lib/python3.14........./site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:861: in create_project_backup
    backup.backup_project(project, user)
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f85206f01a0>
obj = <Project: test0>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_restore_skips_git_hooks
Stack Traces | 1.9s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_restore_skips_git_hooks>

    def test_restore_skips_git_hooks(self) -> None:
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:530: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20b350fc70>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_restore_synthesizes_source_translation_check_flags
Stack Traces | 1.95s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_restore_synthesizes_source_translation_check_flags>

    def test_restore_synthesizes_source_translation_check_flags(self) -> None:
        source = self.component.source_translation
        source.check_flags = "strict-same"
        source.save(update_fields=["check_flags"])
    
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:330: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20b6a021a0>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_restore_rejects_invalid_screenshot
Stack Traces | 2.02s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_restore_rejects_invalid_screenshot>

    def test_restore_rejects_invalid_screenshot(self) -> None:
        screenshot = Screenshot.objects.create(
            name="Tampered screenshot", translation=self.component.source_translation
        )
        with open(TEST_SCREENSHOT, "rb") as handle:
            screenshot.image.save("screenshot.png", File(handle))
    
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:581: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20b06ab070>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_restore_creates_history_entries
Stack Traces | 2.05s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_restore_creates_history_entries>

    def test_restore_creates_history_entries(self) -> None:
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:88: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20b7d8a650>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.api.tests.ProjectAPITest::test_delete
Stack Traces | 2.17s run time
self = <weblate.api.tests.ProjectAPITest testMethod=test_delete>

    def test_delete(self) -> None:
        self.do_request(
            "api:project-detail", self.project_kwargs, method="delete", code=403
        )
>       self.do_request(
            "api:project-detail",
            self.project_kwargs,
            method="delete",
            superuser=True,
            code=204,
        )

weblate/api/tests.py:2287: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
weblate/api/tests.py:162: in do_request
    response = getattr(self.client, method)(
.venv/lib/python3.14.............../site-packages/rest_framework/test.py:307: in delete
    response = super().delete(
.venv/lib/python3.14.............../site-packages/rest_framework/test.py:209: in delete
    return self.generic('DELETE', path, data, content_type, **extra)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.............../site-packages/rest_framework/test.py:221: in generic
    return super().generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.............../site-packages/rest_framework/test.py:273: in request
    return super().request(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.............../site-packages/rest_framework/test.py:225: in request
    request = super().request(**kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1090: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:805: in check_exception
    raise exc_value
.venv/lib/python3.14.../site-packages/rest_framework/views.py:512: in dispatch
    response = handler(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/api/views.py:1574: in destroy
    project_removal.delay(instance.pk, request.user.pk)
.venv/lib/python3.14.../celery/app/task.py:463: in delay
    return self.apply_async(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:624: in apply_async
    return self.apply(args, kwargs, task_id=task_id or uuid(),
.venv/lib/python3.14.../celery/app/task.py:862: in apply
    ret = tracer(task_id, args, kwargs, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:605: in trace_task
    I, R, state, retval = on_error(task_request, exc)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:585: in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:606: in project_removal
    create_project_backup(pk)
.venv/lib/python3.14.../site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:861: in create_project_backup
    backup.backup_project(project, user)
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f5452df41a0>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_views
Stack Traces | 2.2s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_views>

    def test_views(self) -> None:
        start = len(list_backups(self.project))
        url = reverse("backups", kwargs=self.kw_project)
        response = self.client.post(url)
        self.assertEqual(response.status_code, 403)
        self.user.is_superuser = True
        self.user.save()
>       response = self.client.post(url)
                   ^^^^^^^^^^^^^^^^^^^^^

.../trans/tests/test_backups.py:631: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14.../django/test/client.py:1156: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1090: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:805: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:106: in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/utils/decorators.py:48: in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../contrib/auth/decorators.py:59: in _view_wrapper
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/generic/base.py:145: in dispatch
    return handler(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../trans/views/settings.py:493: in post
    create_project_backup.delay(self.obj.pk, request.user.pk)
.venv/lib/python3.14.../celery/app/task.py:463: in delay
    return self.apply_async(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:624: in apply_async
    return self.apply(args, kwargs, task_id=task_id or uuid(),
.venv/lib/python3.14.../celery/app/task.py:862: in apply
    ret = tracer(task_id, args, kwargs, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:605: in trace_task
    I, R, state, retval = on_error(task_request, exc)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:585: in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:861: in create_project_backup
    backup.backup_project(project, user)
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20b052e9c0>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_manage.RemovalTest::test_project
Stack Traces | 2.23s run time
self = <weblate.trans.tests.test_manage.RemovalTest testMethod=test_project>

    def test_project(self) -> None:
        self.make_manager()
        url = reverse("remove", kwargs={"path": self.project.get_url_path()})
        response = self.client.post(url, {"confirm": ""}, follow=True)
        self.assertContains(
            response, "The slug does not match the one marked for deletion!"
        )
>       response = self.client.post(url, {"confirm": "test"}, follow=True)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../trans/tests/test_manage.py:98: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.14.../django/test/client.py:1156: in post
    response = super().post(
.venv/lib/python3.14.../django/test/client.py:499: in post
    return self.generic(
.venv/lib/python3.14.../django/test/client.py:671: in generic
    return self.request(**r)
           ^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../django/test/client.py:1090: in request
    self.check_exception(response)
.venv/lib/python3.14.../django/test/client.py:805: in check_exception
    raise exc_value
.venv/lib/python3.14.../core/handlers/exception.py:55: in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../core/handlers/base.py:198: in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../contrib/auth/decorators.py:59: in _view_wrapper
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../views/decorators/http.py:64: in inner
    return func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
.../trans/views/settings.py:218: in remove
    project_removal.delay(obj.pk, request.user.pk)
.venv/lib/python3.14.../celery/app/task.py:463: in delay
    return self.apply_async(args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:624: in apply_async
    return self.apply(args, kwargs, task_id=task_id or uuid(),
.venv/lib/python3.14.../celery/app/task.py:862: in apply
    ret = tracer(task_id, args, kwargs, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:605: in trace_task
    I, R, state, retval = on_error(task_request, exc)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/trace.py:585: in trace_task
    R = retval = fun(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:606: in project_removal
    create_project_backup(pk)
.venv/lib/python3.14.../site-packages/celery/local.py:182: in __call__
    return self._get_current_object()(*a, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14.../celery/app/task.py:430: in __call__
    return self.run(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
weblate/trans/tasks.py:861: in create_project_backup
    backup.backup_project(project, user)
.../hostedtoolcache/Python/3.14.4....../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f21408dc050>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_backup_creates_history_entry_with_user
Stack Traces | 2.34s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_backup_creates_history_entry_with_user>

    def test_backup_creates_history_entry_with_user(self) -> None:
        backup = ProjectBackup()
    
>       backup.backup_project(self.project, self.user)

.../trans/tests/test_backups.py:80: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20ab0d0b90>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_cleanup
Stack Traces | 2.34s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_cleanup>

    def test_cleanup(self) -> None:
        cleanup_project_backups()
        self.assertLessEqual(len(list_backups(self.project)), 3)
>       ProjectBackup().backup_project(self.project)

.../trans/tests/test_backups.py:609: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20ab28e0d0>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_backup_creates_history_entry
Stack Traces | 2.37s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_backup_creates_history_entry>

    def test_backup_creates_history_entry(self) -> None:
        backup = ProjectBackup()
    
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20a9fcf230>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_restore_batches_change_addon_dispatch
Stack Traces | 2.4s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_restore_batches_change_addon_dispatch>

    def test_restore_batches_change_addon_dispatch(self) -> None:
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:140: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20b6e63790>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_create_duplicate
Stack Traces | 3.57s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_create_duplicate>

    def test_create_duplicate(self) -> None:
        def extract_names(qs) -> list[str]:
            return list(qs.order_by("name").values_list("name", flat=True))
    
        category = self.project.category_set.create(
            name="My Category", slug="my-category"
        )
        self.create_link_existing(name="Test", slug="test", category=category)
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:367: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20abc91940>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError
weblate.trans.tests.test_backups.BackupsTest::test_create_backup
Stack Traces | 3.72s run time
self = <weblate.trans.tests.test_backups.BackupsTest testMethod=test_create_backup>

    def test_create_backup(self) -> None:
        # Create linked component
        self.create_link_existing()
        # Additional content to test on backups
        category = self.project.category_set.create(
            name="My Category", slug="my-category"
        )
        self.component.category = category
        self.component.save()
        label = self.project.label_set.create(name="Label", color="navy")
        unit = self.component.source_translation.unit_set.all()[0]
        unit.labels.add(label)
        shot = Screenshot.objects.create(
            name="Obrazek", translation=self.component.source_translation
        )
        with open(TEST_SCREENSHOT, "rb") as handle:
            shot.image.save("screenshot.png", File(handle))
        shot.add_unit(unit, user=self.user)
    
        unit.comment_set.create(
            comment="Test comment",
            user=self.user,
        )
        suggestion = unit.suggestion_set.create(
            target="Suggestion test",
            user=self.user,
        )
        Vote.objects.create(suggestion=suggestion, user=self.user, value=1)
    
        PendingUnitChange.store_unit_change(unit)
    
        team = Group.objects.create(name="Test group", defining_project=self.project)
        team.roles.set([Role.objects.get(name="Translate")])
        team.admins.add(self.user)
        team.language_selection = SELECTION_MANUAL
        team.languages.set(
            [
                Language.objects.get(code="en"),
                Language.objects.get(code="ru"),
            ]
        )
        AutoGroup(match="^.*$", group=team).save()
    
        backup = ProjectBackup()
>       backup.backup_project(self.project)

.../trans/tests/test_backups.py:206: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.14.4.../x64/lib/python3.14/contextlib.py:85: in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
weblate/trans/backups.py:439: in backup_project
    self.backup_data(project)
weblate/trans/backups.py:226: in backup_data
    "project": self.backup_object(
weblate/trans/backups.py:170: in backup_object
    return {field: self.backup_property(obj, field, extras) for field in properties}
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <weblate.trans.backups.ProjectBackup object at 0x7f20ab520fc0>
obj = <Project: Test>, field = 'set_language_team', extras = None

    def backup_property(
        self, obj: Model, field: str, extras: dict[str, Callable] | None = None
    ) -> str | int | dict | None:
        if extras and field in extras:
            return extras[field](obj)
>       value = getattr(obj, field)
                ^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'Project' object has no attribute 'set_language_team'

weblate/trans/backups.py:144: AttributeError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@nijel
Copy link
Copy Markdown
Member

nijel commented Apr 14, 2026

Yes, exporters honor params since #18830

@gersona gersona changed the title feat(file_format_params): Gettexdt PO header configuration with file format parameters feat(file_format_params): Gettext PO header configuration with file format parameters Apr 14, 2026
@gersona gersona force-pushed the 16971_gettex_settings_as_file_format_params branch from 9d08c23 to bc121b2 Compare April 15, 2026 08:31
@gersona gersona marked this pull request as ready for review April 15, 2026 08:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Gettext PO/POT header configuration via component-level file format parameters, migrating away from the legacy project-level set_language_team setting, and updates related backup/schema/docs/test coverage.

Changes:

  • Introduces new po_* file format parameters to control PO/POT header updates (Language-Team, Last-Translator, X-Generator, Report-Msgid-Bugs-To).
  • Migrates existing Project.set_language_team into component file_format_params and removes the project field + associated UI/API/docs references.
  • Updates backup restore/migration CI scripts and expands tests to validate the new behavior.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
weblate/trans/tests/test_file_format_params.py Adds tests verifying PO header update behavior under the new file format params.
weblate/trans/tests/test_backups.py Verifies backup restore migrates legacy set_language_team into component file format params.
weblate/trans/models/translation.py Uses new gettext file format params to conditionally update PO headers and passes params into update_header.
weblate/trans/models/project.py Removes legacy set_language_team project field.
weblate/trans/migrations/0073_migrate_gettext_header_settings.py Data migration intended to copy legacy project setting into component file_format_params and set defaults.
weblate/trans/migrations/0074_remove_project_set_language_team.py Removes the set_language_team DB field from Project.
weblate/trans/forms.py Removes set_language_team from project forms/tabs.
weblate/trans/fixtures/simple-project.json Drops set_language_team from fixture data.
weblate/trans/file_format_params.py Adds po_* params definitions + registration for gettext header controls.
weblate/trans/backups.py Attempts to migrate legacy set_language_team during restore into component file_format_params.
weblate/formats/txt.py Adjusts type hint for create_new_file language argument to Language.
weblate/formats/ttkit.py Threads file_format_params through untranslate_store/update_header and conditions header fields on the new params.
weblate/formats/tests/test_exporters.py Adds exporter test coverage for toggling Language-Team and X-Generator headers.
weblate/formats/external.py Passes file_format_params to untranslate_store during new file creation (but has a signature/type mismatch).
weblate/formats/exporters.py Makes PO exporter header generation conditional on new params.
weblate/formats/convert.py Adjusts type hint for create_new_file language argument to Language.
weblate/formats/base.py Updates update_header signature and plumbs component file_format_params into format setup.
weblate/api/serializers.py Removes set_language_team from Project API serializer fields.
weblate/addons/tests.py Extends gettext addon tests to cover disabling Report-Msgid-Bugs-To header updates.
weblate/addons/gettext.py Conditions Report-Msgid-Bugs-To updates on the new file format param and passes params to update_header.
docs/specs/schemas/weblate-backup.schema.json Removes legacy set_language_team from backup schema required fields.
docs/snippets/file-format-parameters.rst Documents the new gettext po_* file format parameters.
docs/formats/gettext.rst Updates Gettext docs to reference file format params instead of the legacy project setting.
docs/changes.rst Release notes for new file format params and deprecation/removal of set_language_team.
docs/api.rst Removes set_language_team from documented project API response.
docs/admin/projects.rst Removes the admin documentation section for the old project-level setting.
ci/run-migrate Adds CI migration test setup/assert steps for gettext header settings migration.
ci/migrate-scripts/setup-gettext-header-settings.py Adds migration test setup data for legacy set_language_team -> component params migration.
ci/migrate-scripts/assert-gettext-header-settings.py Asserts migrated component file_format_params contain the expected new keys/values.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +28 to +33
project=project1,
),
Component(
name="gettext-header-settings-component-2",
slug="gettext-header-settings-component-2",
project=project2,
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The created Components don’t specify file_format. For components created with the default empty string, the migration under test (0073) filters on file_format__in=("po", "po-mono"), so these rows won’t be migrated and the corresponding assert script will fail.

Set file_format="po" (and any other minimal required fields) on these Components so the migration test setup actually exercises the gettext header settings migration.

Suggested change
project=project1,
),
Component(
name="gettext-header-settings-component-2",
slug="gettext-header-settings-component-2",
project=project2,
project=project1,
file_format="po",
),
Component(
name="gettext-header-settings-component-2",
slug="gettext-header-settings-component-2",
project=project2,
file_format="po",

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
file_format_params = component.file_format_params or {}
file_format_params["po_set_language_team"] = component.project.set_language_team
file_format_params["po_set_last_translator"] = True
file_format_params["po_set_x_generator"] = True
file_format_params["po_report_msgid_bugs_to"] = True
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this data migration, file_format_params = component.file_format_params or {} will create a new dict when file_format_params is an empty {} (which is the default), and the updated dict is never assigned back to component.file_format_params. As a result, bulk_update() will persist the original (empty) JSON and the migration won’t actually set any of the new po_* keys for many components.

Fix by always copying/mutating and then assigning back, e.g. copy the existing dict (even if empty), set keys on the copy, and set component.file_format_params = copied_dict before adding to to_update.

Suggested change
file_format_params = component.file_format_params or {}
file_format_params["po_set_language_team"] = component.project.set_language_team
file_format_params["po_set_last_translator"] = True
file_format_params["po_set_x_generator"] = True
file_format_params["po_report_msgid_bugs_to"] = True
file_format_params = dict(component.file_format_params or {})
file_format_params["po_set_language_team"] = component.project.set_language_team
file_format_params["po_set_last_translator"] = True
file_format_params["po_set_x_generator"] = True
file_format_params["po_report_msgid_bugs_to"] = True
component.file_format_params = file_format_params

Copilot uses AI. Check for mistakes.
Comment thread weblate/trans/backups.py
Comment on lines +911 to +914
if component.file_format in {"po", "po-mono"}:
component.file_format_params["po_set_language_team"] = (
self.set_language_team
)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.set_language_team defaults to False when the backup doesn’t contain the legacy set_language_team project field, but this block still unconditionally writes po_set_language_team=False for every restored PO component. That can clobber the actual component-level value stored in component.file_format_params in backups created by newer versions, and it also flips the effective default from True to False.

Consider tracking whether the legacy key was present (e.g., store None when absent) and only setting po_set_language_team when restoring an old backup and the component doesn’t already have po_set_language_team set.

Copilot uses AI. Check for mistakes.
Comment thread weblate/trans/backups.py
Comment on lines 1138 to 1142
kwargs = self.data["project"].copy()
kwargs["name"] = project_name
kwargs["slug"] = project_slug
self.set_language_team = kwargs.pop("set_language_team", False)
self.project = project = Project.objects.create(**kwargs)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using kwargs.pop("set_language_team", False) loses the ability to distinguish “old backup didn’t have this field” vs “old backup explicitly had it set to False”. This makes it hard to avoid overwriting component po_set_language_team values when restoring backups created after the migration.

Consider popping with a sentinel/None (and updating the type of self.set_language_team accordingly) so you can conditionally apply the migration only when the legacy field is actually present.

Copilot uses AI. Check for mistakes.
Comment on lines 147 to 164
def create_new_file(
cls,
filename: str,
language: str,
base: str,
callback: Callable | None = None,
file_format_params: FileFormatParams | None = None, # noqa: ARG003
file_format_params: FileFormatParams | None = None,
) -> None:
"""Handle creation of new translation file."""
if not base:
msg = "Not supported"
raise ValueError(msg)
# Parse file
store = cls(base)
if callback:
callback(store)
store.untranslate_store(language)
store.untranslate_store(language, file_format_params=file_format_params)
with open(filename, "wb") as handle:
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XlsxFormat.create_new_file() still accepts language: str and passes that through to store.untranslate_store(...). But XlsxFormat inherits from TTKitFormat via CSVFormat, where untranslate_store() now expects a Language model and accesses language.code. Passing a string here will raise AttributeError when adding a new language for XLSX.

Update this signature to language: Language (matching TranslationFormat.create_new_file) and pass the Language instance through; also consider constructing store = cls(base, file_format_params=file_format_params) for consistency with TTKitFormat.create_new_file.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate gettext specific settings to file format parameters

3 participants