Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/ci/apt-install
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/sh

# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0
#
# System packages for installing Weblate and native extensions in CI (Debian/Ubuntu).
# https://docs.weblate.org/en/latest/admin/install/venv-debian.html#system-requirements

set -e -x

apt-get update

apt-get install -y --no-install-recommends \
build-essential \
git \
gir1.2-pango-1.0 \
gir1.2-rsvg-2.0 \
libcairo-dev \
libffi-dev \
libgirepository-2.0-dev \
libacl1-dev \
liblz4-dev \
libzstd-dev \
libxxhash-dev \
libssl-dev \
libpq-dev \
libjpeg-dev \
libxml2-dev \
libxslt-dev \
libfreetype6-dev \
libyaml-dev \
libz-dev \
python3-dev \
python3-gdbm \
libldap2-dev \
libldap-common \
libsasl2-dev \
libxmlsec1-dev
34 changes: 2 additions & 32 deletions .github/workflows/dep-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,8 @@ jobs:
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: 0.11.12
- name: Install system libraries for Weblate (venv Debian/Ubuntu)
run: |
sudo apt-get update
# https://docs.weblate.org/en/latest/admin/install/venv-debian.html#system-requirements
sudo apt-get install -y --no-install-recommends \
build-essential \
git \
gir1.2-pango-1.0 \
gir1.2-rsvg-2.0 \
libcairo-dev \
libffi-dev \
libgirepository-2.0-dev \
libacl1-dev \
liblz4-dev \
libzstd-dev \
libxxhash-dev \
libssl-dev \
libpq-dev \
libjpeg-dev \
libxml2-dev \
libxslt-dev \
libfreetype6-dev \
libyaml-dev \
libz-dev \
python3-dev \
python3-gdbm
# Optional:
sudo apt-get install -y --no-install-recommends \
libldap2-dev \
libldap-common \
libsasl2-dev \
libxmlsec1-dev
- name: Install apt dependencies (Weblate venv)
run: sudo ./.github/ci/apt-install
- name: Install project and tools
run: |
uv pip install --system -e .
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/lint-and-format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
version: 0.11.12
- name: Install apt dependencies (Weblate venv)
run: sudo ./.github/ci/apt-install
- name: pre-commit
run: |
uv run --only-group pre-commit prek run --all-files --show-diff-on-failure
Expand Down
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ repos:
rev: v1.7.12
hooks:
- id: actionlint
- repo: local
hooks:
- id: pytest
name: pytest
entry: uv run --group pre-commit pytest
language: system
pass_filenames: false
ci:
autoupdate_schedule: quarterly
skip:
Expand Down
45 changes: 45 additions & 0 deletions docs/plugin-http-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--
SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>

SPDX-License-Identifier: BSL-1.0
-->

# Why the plugin must register its own URLs

This answers [Boost Weblate Plugin Refactor — Plan](boost-weblate-plugin-refactor-plan.md) (§ URL registration): Weblate’s `urls.py` does **not** auto-discover URLconfs from arbitrary `INSTALLED_APPS` entries; the plugin must attach routes explicitly.

Read **`boost-weblate/weblate/urls.py`** (same layout as [upstream `weblate/urls.py`](https://github.com/WeblateOrg/weblate/blob/main/weblate/urls.py)):

1. **Single hand-built list.** Routes are collected in **`real_patterns`**, starting with concrete `path(...)` entries—not Django’s per-app `urls.py` autoload.

```74:76:boost-weblate/weblate/urls.py
real_patterns = [
path("", weblate.trans.views.dashboard.home, name="home"),
path("projects/", weblate.trans.views.basic.list_projects, name="projects"),
```

2. **Optional apps only by name.** Extra routes appear when `urls.py` **explicitly** checks for known dotted apps in `settings.INSTALLED_APPS`, then mutates **`real_patterns`** (same file; examples include legal, git export, SAML—each `if "…" in settings.INSTALLED_APPS:` followed by `real_patterns +=` / `append`).

```1041:1047:boost-weblate/weblate/urls.py
if "weblate.legal" in settings.INSTALLED_APPS:
real_patterns.extend(
(
path(
"legal/",
include(("weblate.legal.urls", "weblate.legal"), namespace="legal"),
```

3. **Final URLconf.** Either `urlpatterns = real_patterns` or, when `URL_PREFIX` is set, a wrapper `include(real_patterns)`—still no generic scan of your plugin package.

```1091:1095:boost-weblate/weblate/urls.py
# Handle URL prefix configuration
if not URL_PREFIX:
urlpatterns = real_patterns
else:
urlpatterns = [path(URL_PREFIX, include(real_patterns))]
```

**Conclusion:** Putting **`boost_weblate.endpoint`** in `INSTALLED_APPS` does not register HTTP routes by itself. The plan’s two supported approaches are:

- **`AppConfig.ready()`** — at startup, extend Weblate’s pattern list (this repo appends to **`weblate.urls.real_patterns`** in **`src/boost_weblate/endpoint/apps.py`**), **or**
- **`ROOT_URLCONF`** in **`boost_weblate/settings_override.py`** (copied to **`/app/data/settings-override.py`** in Docker) — a custom root URLconf that includes Weblate’s patterns and adds the plugin’s `include()`.
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ lint = [
{include-group = "pre-commit"}
]
pre-commit = [
"prek==0.3.13"
"prek==0.3.13",
"pytest>=8.3"
]
tooling = [
{include-group = "lint"}
Expand Down Expand Up @@ -55,7 +56,8 @@ version = "0.1.0"

[project.optional-dependencies]
dev = [
"prek==0.3.13"
"prek==0.3.13",
"pytest>=8.3"
]

[project.urls]
Expand Down Expand Up @@ -100,6 +102,10 @@ authorized_packages = {pyaskalono = ">=0.2.0"}
level = "cautious"
unauthorized_licenses = []

[tool.pytest.ini_options]
pythonpath = ["src", "."]
testpaths = ["tests"]

[tool.ruff]
line-length = 88
target-version = "py312"
Expand All @@ -115,5 +121,5 @@ source-include = [
"LICENSES/**",
"REUSE.toml",
"uv.lock",
"settings-override.py"
"tests/**"
]
5 changes: 5 additions & 0 deletions src/boost_weblate/endpoint/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""Boost HTTP API mounted on the Weblate site under ``/boost-endpoint/``."""
59 changes: 59 additions & 0 deletions src/boost_weblate/endpoint/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

from __future__ import annotations

import logging

from django.apps import AppConfig
from django.urls import include, path

logger = logging.getLogger(__name__)

_PLUGIN_URLS_ATTR = "_cppa_boost_weblate_urls_registered"


def register_plugin_urls() -> None:
"""Append this app's routes to Weblate's pattern list.

Weblate builds ``urlpatterns`` from module-level ``real_patterns`` (see
``weblate.urls``). Optional integrations append to ``real_patterns`` before
the ``URL_PREFIX`` wrapper is applied, so mutating that list keeps routes
consistent when a path prefix is configured.
"""
try:
import weblate.urls as wl_urls # noqa: PLC0415
except ModuleNotFoundError as exc:
logger.debug(
"boost_weblate.endpoint: skipping URL registration (import error: %s)",
exc,
)
return

if getattr(wl_urls, _PLUGIN_URLS_ATTR, False):
return

if not hasattr(wl_urls, "real_patterns"):
logger.warning(
"boost_weblate.endpoint: weblate.urls has no real_patterns; "
"URL registration skipped (unexpected Weblate layout)."
)
return

wl_urls.real_patterns.append(
path("boost-endpoint/", include("boost_weblate.endpoint.urls")),
)
setattr(wl_urls, _PLUGIN_URLS_ATTR, True)


class BoostEndpointConfig(AppConfig):
"""Registers ``/boost-endpoint/`` on the Weblate URLconf when the app loads."""

default_auto_field = "django.db.models.BigAutoField"
name = "boost_weblate.endpoint"
label = "boost_endpoint"
verbose_name = "Boost documentation translation API"

def ready(self) -> None:
register_plugin_urls()
5 changes: 5 additions & 0 deletions src/boost_weblate/endpoint/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""DRF serializers for the Boost documentation translation API."""
5 changes: 5 additions & 0 deletions src/boost_weblate/endpoint/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""Service layer for the Boost documentation translation API."""
17 changes: 17 additions & 0 deletions src/boost_weblate/endpoint/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""URL patterns mounted under ``/boost-endpoint/`` on the Weblate site."""

from __future__ import annotations

from django.urls import path

from boost_weblate.endpoint import views

app_name = "boost_endpoint"

urlpatterns = [
path("plugin-ping/", views.plugin_ping, name="plugin-ping"),
]
14 changes: 14 additions & 0 deletions src/boost_weblate/endpoint/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

from __future__ import annotations

from django.http import HttpResponse
from django.views.decorators.http import require_GET


@require_GET
def plugin_ping(_request):
"""Minimal health-style endpoint for URL registration smoke tests."""
return HttpResponse("ok", content_type="text/plain")
2 changes: 1 addition & 1 deletion src/boost_weblate/formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""Weblate translation format handlers for Boost (QuickBook and related)."""

__all__: list[str] = []
__all__: list[str] = ["QuickBookFormat"]
74 changes: 74 additions & 0 deletions src/boost_weblate/formats/quickbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SPDX-FileCopyrightText: 2026 Andrew Zhang <whisper67265@outlook.com>
#
# SPDX-License-Identifier: BSL-1.0

"""QuickBook file format adapter for upstream Weblate.

Thin :class:`~weblate.formats.convert.ConvertFormat` subclass that delegates
parsing to :class:`~boost_weblate.utils.quickbook.QuickBookFile` and
reconstruction to :class:`~boost_weblate.utils.quickbook.QuickBookTranslator`.
Same control flow as ``AsciiDocFormat`` in ``weblate.formats.convert``.
"""

from __future__ import annotations

from typing import IO, TYPE_CHECKING

from django.utils.translation import gettext_lazy
from weblate.formats.convert import ConvertFormat
from weblate.formats.helpers import NamedBytesIO

from boost_weblate.utils.quickbook import QuickBookFile, QuickBookTranslator

if TYPE_CHECKING:
from translate.storage.base import TranslationStore
from weblate.formats.base import TranslationFormat


class QuickBookFormat(ConvertFormat):
"""QuickBook (.qbk) documentation file format."""

# Translators: File format name
name = gettext_lazy("QuickBook file")
autoload = ("*.qbk",)
format_id = "quickbook"
monolingual = True

def convertfile(
self,
storefile: IO[bytes],
template_store: TranslationFormat | None,
) -> TranslationStore:
qbkparser = QuickBookFile(inputfile=NamedBytesIO("", storefile.read()))

duplicate_style = "msgctxt"
if self.file_format_params.get("merge_duplicates"):
duplicate_style = "merge"

return self.convert_to_po(
qbkparser, template_store, duplicate_style=duplicate_style
)

def save_content(self, handle: IO[bytes]) -> None:
"""Store content to file."""
converter = QuickBookTranslator(
inputstore=self.store, includefuzzy=True, outputthreshold=None
)
if self.template_store is None:
msg = "Template store is required."
raise TypeError(msg)
templatename = self.template_store.storefile
if hasattr(templatename, "name"):
templatename = templatename.name
with open(templatename, "rb") as templatefile:
converter.translate(templatefile, handle)

@staticmethod
def mimetype() -> str:
"""Return most common mime type for format."""
return "text/x-quickbook"

@staticmethod
def extension() -> str:
"""Return most common file extension for format."""
return "qbk"
Loading