Skip to content

Commit 10418c6

Browse files
committed
feat(add-ons): add pot update support
- currently limited to xgettext, Django and Sphinx - we have not intention for generic solution here Fixes #10708 Fixes #18428
1 parent c480d3f commit 10418c6

File tree

20 files changed

+2302
-1
lines changed

20 files changed

+2302
-1
lines changed

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Weblate 5.17
88
* Added :setting:`WEBSITE_ALERTS_ENABLED` setting to allow disabling project website availability checks and alerts.
99
* Shared components can now be categorized within the target project.
1010
* :ref:`api` supports specifying a category when sharing a component via ``category_id`` parameter.
11+
* Added :ref:`addon-weblate-gettext-xgettext`, :ref:`addon-weblate-gettext-django`, and :ref:`addon-weblate-gettext-sphinx` to update POT files with configurable update cadence.
1112

1213
.. rubric:: Improvements
1314

docs/snippets/addons-autogenerated.rst

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,3 +1868,104 @@ Change events
18681868

18691869

18701870
.. AUTOGENERATED END: addon-parameters
1871+
.. AUTOGENERATED START: weblate.gettext.django
1872+
.. This section is automatically generated by `./manage.py list_addons`. Do not edit manually.
1873+
1874+
.. _addon-weblate.gettext.django:
1875+
1876+
Update POT file (Django)
1877+
------------------------
1878+
1879+
.. versionadded:: 5.17
1880+
1881+
:Add-on ID: ``weblate.gettext.django``
1882+
:Configuration: +----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1883+
| ``interval`` | Update frequency | How often the add-on should update the POT file when the component is refreshed. |
1884+
| | | |
1885+
| | | .. list-table:: Available choices: |
1886+
| | | :width: 100% |
1887+
| | | |
1888+
| | | * - ``daily`` |
1889+
| | | - Daily |
1890+
| | | * - ``weekly`` |
1891+
| | | - Weekly |
1892+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1893+
| ``normalize_header`` | Normalize POT header | Updates selected gettext header fields using component configuration. |
1894+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1895+
| ``update_po_files`` | Update PO files using msgmerge | Ensures the msgmerge add-on is installed and triggers it after the POT file is updated. |
1896+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1897+
1898+
:Triggers: :ref:`addon-event-add-on-installation`, :ref:`addon-event-repository-post-update`
1899+
1900+
Updates the gettext template using Django's built-in makemessages command.
1901+
1902+
.. AUTOGENERATED END: weblate.gettext.django
1903+
.. AUTOGENERATED START: weblate.gettext.sphinx
1904+
.. This section is automatically generated by `./manage.py list_addons`. Do not edit manually.
1905+
1906+
.. _addon-weblate.gettext.sphinx:
1907+
1908+
Update POT file (Sphinx)
1909+
------------------------
1910+
1911+
.. versionadded:: 5.17
1912+
1913+
:Add-on ID: ``weblate.gettext.sphinx``
1914+
:Configuration: +----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1915+
| ``interval`` | Update frequency | How often the add-on should update the POT file when the component is refreshed. |
1916+
| | | |
1917+
| | | .. list-table:: Available choices: |
1918+
| | | :width: 100% |
1919+
| | | |
1920+
| | | * - ``daily`` |
1921+
| | | - Daily |
1922+
| | | * - ``weekly`` |
1923+
| | | - Weekly |
1924+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1925+
| ``normalize_header`` | Normalize POT header | Updates selected gettext header fields using component configuration. |
1926+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1927+
| ``update_po_files`` | Update PO files using msgmerge | Ensures the msgmerge add-on is installed and triggers it after the POT file is updated. |
1928+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1929+
1930+
:Triggers: :ref:`addon-event-add-on-installation`, :ref:`addon-event-repository-post-update`
1931+
1932+
Updates the gettext template using Sphinx's gettext builder without loading
1933+
project configuration.
1934+
1935+
.. AUTOGENERATED END: weblate.gettext.sphinx
1936+
.. AUTOGENERATED START: weblate.gettext.xgettext
1937+
.. This section is automatically generated by `./manage.py list_addons`. Do not edit manually.
1938+
1939+
.. _addon-weblate.gettext.xgettext:
1940+
1941+
Update POT file (xgettext)
1942+
--------------------------
1943+
1944+
.. versionadded:: 5.17
1945+
1946+
:Add-on ID: ``weblate.gettext.xgettext``
1947+
:Configuration: +----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1948+
| ``interval`` | Update frequency | How often the add-on should update the POT file when the component is refreshed. |
1949+
| | | |
1950+
| | | .. list-table:: Available choices: |
1951+
| | | :width: 100% |
1952+
| | | |
1953+
| | | * - ``daily`` |
1954+
| | | - Daily |
1955+
| | | * - ``weekly`` |
1956+
| | | - Weekly |
1957+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1958+
| ``normalize_header`` | Normalize POT header | Updates selected gettext header fields using component configuration. |
1959+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1960+
| ``update_po_files`` | Update PO files using msgmerge | Ensures the msgmerge add-on is installed and triggers it after the POT file is updated. |
1961+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1962+
| ``language`` | xgettext language | Programming language passed to xgettext, for example Python or C. |
1963+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1964+
| ``files`` | Source file patterns | Newline-separated repository-relative glob patterns for files to extract with xgettext. |
1965+
+----------------------+--------------------------------+-----------------------------------------------------------------------------------------+
1966+
1967+
:Triggers: :ref:`addon-event-add-on-installation`, :ref:`addon-event-repository-post-update`
1968+
1969+
Updates the gettext template using xgettext on selected source files.
1970+
1971+
.. AUTOGENERATED END: weblate.gettext.xgettext

weblate/addons/extractors/__init__.py

Whitespace-only changes.

weblate/addons/extractors/django/__init__.py

Whitespace-only changes.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Copyright © Michal Čihař <michal@weblate.org>
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from __future__ import annotations
6+
7+
import os
8+
9+
from django.conf import settings
10+
from django.core.management.base import CommandError
11+
from django.core.management.commands.makemessages import check_programs
12+
from django.core.management.utils import handle_extensions
13+
from django.utils.text import get_text_list
14+
15+
from weblate.utils.management.commands.makemessages import Command as BaseCommand
16+
17+
18+
class Command(BaseCommand):
19+
"""
20+
Extraction-only variant of makemessages using trusted output paths.
21+
22+
Unlike stock ``makemessages``, this command never discovers repository
23+
``locale`` or ``conf/locale`` directories as output locations. Persistent
24+
gettext output is routed to the trusted paths provided in
25+
``settings.LOCALE_PATHS`` rather than to repository locale directories.
26+
27+
This guarantee relies on the inherited ``find_files()`` implementation
28+
continuing to honor ``ignore_patterns`` for ``locale`` directories before
29+
reaching Django's special-case locale directory handling.
30+
31+
The command line is intentionally narrow because this is an internal
32+
extractor entry point used by Weblate add-ons, not a general replacement
33+
for Django's ``makemessages`` command.
34+
"""
35+
36+
EXTRA_IGNORE_PATTERNS = (
37+
"CVS",
38+
".*",
39+
"*~",
40+
"*.pyc",
41+
".git/*",
42+
".venv/*",
43+
"venv/*",
44+
"node_modules/*",
45+
"build/*",
46+
"dist/*",
47+
"locale",
48+
"conf/locale",
49+
)
50+
51+
def add_arguments(self, parser):
52+
"""
53+
Register only the internal CLI supported by this extractor.
54+
55+
This intentionally replaces Django's default ``makemessages`` argument
56+
set and does not call ``super().add_arguments()``. The command is used
57+
only by Weblate's internal add-on integration and should not expose
58+
the broader stock management command interface.
59+
"""
60+
parser.add_argument(
61+
"-d",
62+
"--domain",
63+
choices=("django", "djangojs"),
64+
required=True,
65+
)
66+
parser.add_argument("--no-wrap", action="store_true")
67+
parser.add_argument("--no-location", action="store_true")
68+
69+
def handle(self, *args, **options):
70+
self.domain = options["domain"]
71+
self.verbosity = options["verbosity"]
72+
self.symlinks = False
73+
self.ignore_patterns = list(self.EXTRA_IGNORE_PATTERNS)
74+
75+
if options["no_wrap"]:
76+
self.msgmerge_options = [*self.msgmerge_options, "--no-wrap"]
77+
self.msguniq_options = [*self.msguniq_options, "--no-wrap"]
78+
self.msgattrib_options = [*self.msgattrib_options, "--no-wrap"]
79+
self.xgettext_options = [*self.xgettext_options, "--no-wrap"]
80+
if options["no_location"]:
81+
self.msgmerge_options = [*self.msgmerge_options, "--no-location"]
82+
self.msguniq_options = [*self.msguniq_options, "--no-location"]
83+
self.msgattrib_options = [*self.msgattrib_options, "--no-location"]
84+
self.xgettext_options = [*self.xgettext_options, "--no-location"]
85+
86+
self.no_obsolete = False
87+
self.keep_pot = True
88+
89+
exts = ["js"] if self.domain == "djangojs" else ["html", "txt", "py"]
90+
self.extensions = handle_extensions(exts)
91+
92+
if self.verbosity > 1:
93+
self.stdout.write(
94+
"examining files with the extensions: "
95+
f"{get_text_list(list(self.extensions), 'and')}"
96+
)
97+
98+
self.invoked_for_django = False
99+
self.locale_paths = []
100+
self.default_locale_path = None
101+
for path in settings.LOCALE_PATHS:
102+
locale_path = os.path.abspath(path)
103+
if locale_path not in self.locale_paths:
104+
self.locale_paths.append(locale_path)
105+
if not self.locale_paths:
106+
msg = "Missing trusted locale output path for extraction."
107+
raise CommandError(msg)
108+
self.default_locale_path = self.locale_paths[0]
109+
os.makedirs(self.default_locale_path, exist_ok=True)
110+
111+
check_programs("xgettext", "msguniq")
112+
113+
self.build_potfiles()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright © Michal Čihař <michal@weblate.org>
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from __future__ import annotations
6+
7+
import os
8+
import sys
9+
10+
import django
11+
12+
os.environ.setdefault(
13+
"DJANGO_SETTINGS_MODULE", "weblate.addons.extractors.django.settings"
14+
)
15+
16+
17+
def main() -> None:
18+
django.setup()
19+
20+
from weblate.addons.extractors.django.command import Command
21+
22+
Command().run_from_argv(
23+
[
24+
sys.executable,
25+
"weblate-extract-makemessages",
26+
*sys.argv[1:],
27+
]
28+
)
29+
30+
31+
if __name__ == "__main__":
32+
main()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright © Michal Čihař <michal@weblate.org>
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
"""Minimal Django settings for extraction-only management commands."""
6+
7+
import os
8+
9+
from django.core.management.utils import get_random_secret_key
10+
11+
SECRET_KEY = get_random_secret_key()
12+
USE_I18N = True
13+
LOGGING_CONFIG = None
14+
LOCALE_FILTER_FILES = False
15+
INSTALLED_APPS: list[str] = []
16+
LOCALE_PATHS = [os.environ["WEBLATE_EXTRACT_LOCALE_PATH"]]

weblate/addons/extractors/sphinx/__init__.py

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright © Michal Čihař <michal@weblate.org>
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
"""Minimal Sphinx configuration for extraction-only builds."""
6+
7+
project = "Documentation"
8+
extensions = []
9+
exclude_patterns = ["_build", "locales"]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-FileCopyrightText: Michal Čihař <michal@weblate.org>
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
[general]
6+
file_insertion_enabled: 0
7+
raw_enabled: 0

0 commit comments

Comments
 (0)