diff --git a/codecov.yml b/codecov.yml
index 3cbdafcf6b99..f19acfc11f5d 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -23,7 +23,6 @@ coverage:
target: 90
patch:
default:
- informational: true
target: 100
codecov:
branch: main
diff --git a/docs/admin/boost-weblate.rst b/docs/admin/boost-weblate.rst
new file mode 100644
index 000000000000..b5db99954c5b
--- /dev/null
+++ b/docs/admin/boost-weblate.rst
@@ -0,0 +1,189 @@
+.. _boost-weblate:
+
+Boost Weblate additions
+=======================
+
+This repository extends upstream Weblate with capabilities used for translating
+`Boost C++ Libraries `_ documentation: QuickBook and
+AsciiDoc handling tailored for Boost workflows, optional OpenRouter-based batch
+machine translation, and a REST surface for CI-driven component maintenance.
+
+The sections below document fork-specific **configuration**, **dependencies**,
+and **HTTP endpoints** that are not covered by generic Weblate documentation.
+
+.. seealso::
+
+ Standard administrator guides still apply: :doc:`install/docker`,
+ :ref:`docker-environment`, :doc:`machine`, and :doc:`config`.
+
+Python packages
+---------------
+
+OpenRouter batch translation uses the `OpenAI Python SDK `_
+(`OpenAI Client`) against the OpenRouter HTTP API. The SDK is **not** part of
+core Weblate dependencies; install it explicitly:
+
+.. code-block:: sh
+
+ pip install 'weblate[openai]'
+ # or
+ pip install 'openai>=2.0,<3.0'
+
+If the SDK is missing when OpenRouter translation runs, Weblate raises
+``django.core.exceptions.ImproperlyConfigured`` with an installation hint.
+
+Docker images built from :file:`weblate-docker/Dockerfile` use
+``WEBLATE_EXTRAS=all`` so the ``openai`` extra is included in the container.
+
+System commands and packages
+----------------------------
+
+The following executables must be available on the server **PATH** where the
+relevant code paths execute (web workers, Celery workers):
+
+.. list-table::
+ :header-rows: 1
+ :widths: 22 78
+
+ * - Executable
+ - Used by
+ * - ``git``
+ - Boost endpoint service: clone repositories, commit and push translation
+ changes (see ``weblate.boost_endpoint.services``).
+ * - ``po4a-gettextize``, ``po4a-translate``
+ - AsciiDoc format pipeline (``weblate.formats.asciidoc``).
+ * - ``msgattrib``, ``msgfmt``
+ - gettext toolchain for AsciiDoc save path; ``msgattrib`` is optional (the
+ code falls back if absent).
+
+The official Docker image for this fork installs **po4a** from source during the
+image build (see comments in :file:`weblate-docker/Dockerfile`). Custom or
+bare-metal installs must provide **po4a** and **gettext** separately (for
+example distribution packages for ``po4a`` and ``gettext``).
+
+Environment variables
+---------------------
+
+These variables apply to **Boost fork** behaviour. They do **not** use the
+``WEBLATE_`` prefix. Standard Docker variables remain documented under
+:ref:`docker-environment`.
+
+.. envvar:: OPENROUTER_API_KEY
+
+ API key used when OpenRouter batch translation cannot read credentials from
+ Weblate’s machinery configuration (see :ref:`boost-weblate-openrouter-config`).
+ Read by ``weblate.trans.autobatchtranslate``.
+
+.. envvar:: OPENROUTER_MODEL
+
+ Model identifier passed to OpenRouter (for example ``deepseek/deepseek-chat``).
+ Default if unset: ``deepseek/deepseek-chat``. Used together with
+ :envvar:`OPENROUTER_API_KEY` as an environment fallback.
+
+.. envvar:: AUTO_BATCH_TRANSLATE_VIA_OPENROUTER
+
+ Boolean interpreted by :file:`weblate/settings_docker.py`. When ``true``
+ (Docker default), components may trigger automatic batch translation via
+ OpenRouter according to internal workflows. When ``false``, that behaviour is
+ disabled. For non-Docker installs, set ``AUTO_BATCH_TRANSLATE_VIA_OPENROUTER``
+ in :file:`settings.py`.
+
+.. envvar:: BOOST_ENDPOINT_ADD_TRANSLATION_SECONDS
+
+ Integer seconds to wait when the Boost endpoint waits for a component or
+ translation to become ready before adding a language (polling interval is
+ derived from this setting in ``weblate.boost_endpoint.services``).
+ Default in Docker: ``300``. Override per deployment if repositories are slow
+ or fast to sync.
+
+.. _boost-weblate-openrouter-config:
+
+OpenRouter credentials (batch translation)
+------------------------------------------
+
+Batch OpenRouter translation resolves configuration in this order:
+
+#. **Weblate machinery settings** — category MT, ``openai`` entry with ``key``
+ (API key) and ``custom_model`` (model id). This mirrors fields used for the
+ generic OpenAI-compatible machinery documented under :ref:`mt-openai`.
+#. **Environment variables** — :envvar:`OPENROUTER_API_KEY` and
+ :envvar:`OPENROUTER_MODEL` when the database configuration does not supply both
+ values.
+
+If no usable key and model are found, auto-translation is skipped and a warning
+is logged.
+
+REST API: ``/boost-endpoint/``
+-------------------------------
+
+These endpoints are **not** part of the ``/api/`` namespace and are **not**
+included in the OpenAPI schema served at ``/api/schema/``. They require an
+authenticated user (same token mechanism as :ref:`api-tokens`).
+
+Base path (relative to your site root): ``/boost-endpoint/``.
+
+.. http:get:: /boost-endpoint/
+
+ Returns a short JSON description of the Boost endpoint module.
+
+ :reqheader Authorization: ``Token …`` (required)
+
+ :status 200:
+
+ .. code-block:: json
+
+ {
+ "module": "boost-endpoint",
+ "description": "Boost documentation translation API"
+ }
+
+.. http:post:: /boost-endpoint/add-or-update/
+
+ Accepts a job description and enqueues asynchronous work on a Celery worker.
+ The HTTP response returns immediately with a task identifier.
+
+ :reqheader Authorization: ``Token …`` (required)
+ :reqheader Content-Type: ``application/json``
+
+ :",
+ "detail": "Boost add-or-update is running in the background; check Celery logs or task result for completion."
+ }
+
+ :status 400: Validation error.
+
+ .. code-block:: json
+
+ { "errors": { "...": ["..."] } }
+
+Related Django settings
+-----------------------
+
+The following settings appear in :file:`weblate/settings_example.py` for
+non-Docker deployments:
+
+``AUTO_BATCH_TRANSLATE_VIA_OPENROUTER``
+ Enables or disables OpenRouter batch translation hooks. Defaults to
+ ``False`` in the example settings file; Docker defaults differ via
+ :envvar:`AUTO_BATCH_TRANSLATE_VIA_OPENROUTER`.
+
+``BOOST_ENDPOINT_ADD_TRANSLATION_SECONDS``
+ Delay used when waiting for components during Boost endpoint processing.
+ Example file sets ``150`` seconds; Docker overrides via
+ :envvar:`BOOST_ENDPOINT_ADD_TRANSLATION_SECONDS` unless customised.
+
+File formats
+------------
+
+* :doc:`../formats/quickbook` — QuickBook ``.qbk`` (fork-specific).
+* :doc:`../formats/asciidoc` — AsciiDoc (implementation notes including **po4a**).
diff --git a/docs/admin/machine.rst b/docs/admin/machine.rst
index 579423def322..63e305765528 100644
--- a/docs/admin/machine.rst
+++ b/docs/admin/machine.rst
@@ -369,15 +369,15 @@ Weblate supports DeepL formality, it will choose matching one based on the
language (for example, there is ``de@formal`` and ``de@informal``).
The translation context can optionally be specified to improve translations quality. Read more on that in
-`DeepL translation context documentation `_.
+`DeepL translation context documentation `_.
The service automatically uses :ref:`glossary`, see :ref:`glossary-mt`.
.. seealso::
- * `DeepL translator `_
- * `DeepL pricing `_
- * `DeepL API documentation `_
+ * `DeepL translator `_
+ * `DeepL pricing `_
+ * `DeepL API documentation `_
.. _mt-glosbe:
@@ -622,7 +622,7 @@ You can also specify a custom category to use `custom translator `_
- * `Microsoft Azure Portal `_
+ * `Microsoft Azure Portal `_
* `Base URLs `_
* `"Authenticating with a Multi-service resource" `_
* `"Authenticating with an access token" section `_
@@ -1076,7 +1076,7 @@ This service uses an API, and you need to obtain an ID and an API key from Youda
.. seealso::
- `Youdao Zhiyun Natural Language Translation Service `_
+ `Youdao Zhiyun Natural Language Translation Service `_
.. _custom-machinery:
diff --git a/docs/api.rst b/docs/api.rst
index 93a4f90c8fc8..d5f242ae02dc 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -20,6 +20,11 @@ can browse at ``/api/docs/``.
incomplete at this point and subject to change. Please consult the
documentation below for more detailed information on the API.
+.. note::
+
+ **Boost Weblate fork:** authenticated endpoints under ``/boost-endpoint/``
+ (outside ``/api/``) are documented in :doc:`admin/boost-weblate`.
+
.. _api-generic:
Authentication and generic parameters
diff --git a/docs/conf.py b/docs/conf.py
index bf45f9d520bb..3183715ff190 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -361,7 +361,53 @@ def setup(app) -> None:
# Number of retries and timeout for linkcheck
linkcheck_retries = 10
-linkcheck_timeout = 10
+# Default 10s is tight for some CDNs from GitHub Actions (e.g. contributor-covenant.org).
+linkcheck_timeout = 45
+
+# Treat these redirects as working. Linkcheck emits redirects as warnings; CI runs
+# sphinx with --fail-on-warning, so expected HTTP redirects must be allowed here
+# (or links updated in RST). Patterns use re.match() from the URL start.
+linkcheck_allowed_redirects = {
+ (
+ r"https://support\.okta\.com/help/s/article/"
+ r"How-to-send-a-custom-relaystate-to-application-through-idp-initiated-authentication-urls"
+ ): (
+ r"https://support\.okta\.com/help/s/article/"
+ r"How-to-send-a-custom-relaystate-to-application-through-idp-initiated-authentication-urls(\?.*)?"
+ ),
+ r"https://docs\.djangoproject\.com/en/stable/.*": (
+ r"https://docs\.djangoproject\.com/en/[0-9]+\.[0-9]+/.*"
+ ),
+ r"https://weblate\.org/?.*": r"https://weblate\.org/.*",
+ r"https://docs\.weblate\.org/?.*": r"https://docs\.weblate\.org/.*",
+ r"https://hosted\.weblate\.org.*": r"https://hosted\.weblate\.org.*",
+ r"https://www\.sphinx-doc\.org/?$": r"https://www\.sphinx-doc\.org/en/master/?",
+ r"https://angular\.io/.*": r"https://.*\.angular\.io/.*",
+ r"https://babel\.pocoo\.org/?$": r"https://babel\.pocoo\.org/en/latest/.*",
+ r"https://cryptography\.io/?$": r"https://cryptography\.io/en/latest/.*",
+ r"https://docs\.celeryq\.dev/?$": r"https://docs\.celeryq\.dev/en/stable/.*",
+ r"https://docs\.phpmyadmin\.net/?$": r"https://docs\.phpmyadmin\.net/en/latest/.*",
+ r"https://doc\.galette\.eu/?$": r"https://doc\.galette\.eu/en/master/.*",
+ r"https://pytest\.org/?$": r"https://docs\.pytest\.org/.*",
+ r"https://python-social-auth\.readthedocs\.io/?$": (
+ r"https://python-social-auth\.readthedocs\.io/en/latest/.*"
+ ),
+ r"https://sentry\.io/?$": r"https://sentry\.io/.*",
+ r"https://ruby-doc\.org/current/.*": r"https://ruby-doc\.org/[0-9.]+/.*",
+ r"https://docs\.anthropic\.com/.*": r"https://.*\.claude\.com/.*",
+ r"https://console\.anthropic\.com/.*": r"https://.*\.claude\.com/.*",
+ r"https://console\.cloud\.google\.com/.*": r"https://accounts\.google\.com/.*",
+ r"https://console\.developers\.google\.com/.*": r"https://accounts\.google\.com/.*",
+ r"https://gitee\.com/help/.*": r"https://help\.gitee\.com.*",
+ r"https://git\.cloudron\.io/.*": r"https://git\.cloudron\.io/.*",
+ r"https://github\.com/[^/]+/[^/]+/security/advisories/new(\?.*)?$": (
+ r"https://github\.com/login.*"
+ ),
+ r"https://www\.bestpractices\.dev/en/projects/[0-9]+/?$": (
+ r"https://www\.bestpractices\.dev/en/projects/[0-9]+/passing"
+ ),
+}
+
linkcheck_ignore = [
# Local URL to Weblate
"http://127.0.0.1:8080/",
@@ -402,6 +448,11 @@ def setup(app) -> None:
"https://dev.mysql.com/",
# Responds with HTTP 418 I'm a teapot
"https://www.freedesktop.org/",
+ # 403 to automated clients (URLs remain valid in browsers)
+ "https://mymemory\\.translated\\.net/.*",
+ "https://docs\\.oasis-open\\.org/.*",
+ # Captcha / bot wall in CI; human documentation links remain valid
+ "https://cloud\\.yandex\\.com/.*",
]
# HTTP docs
diff --git a/docs/formats.rst b/docs/formats.rst
index e2483a36e95e..5ac8a13a1e27 100644
--- a/docs/formats.rst
+++ b/docs/formats.rst
@@ -346,6 +346,22 @@ Translation types capabilities
- no
- no
-
+ * - :ref:`asciidoc`
+ - mono
+ - no
+ - no
+ - no
+ - no
+ - no
+ -
+ * - :ref:`quickbook`
+ - mono
+ - no
+ - no
+ - no
+ - no
+ - no
+ -
.. [#m] See :ref:`bimono`
.. [#p] See :ref:`format-plurals`
@@ -359,7 +375,7 @@ Translation types capabilities
.. [#lp] The plurals are supported only for Laravel which uses in string syntax to define them, see `Localization in Laravel`_.
.. [#fp] Plurals are handled in the syntax of the strings and not exposed as plurals in Weblate.
-.. _Localization in Laravel: https://laravel.com/docs/localization
+.. _Localization in Laravel: https://laravel.com/docs/13.x/localization
.. _bimono:
diff --git a/docs/formats/asciidoc.rst b/docs/formats/asciidoc.rst
index 4d8a2d5f6e0c..b74cb6fac4d2 100644
--- a/docs/formats/asciidoc.rst
+++ b/docs/formats/asciidoc.rst
@@ -11,6 +11,17 @@ The translatable content is extracted from the AsciiDoc files and offered for th
.. include:: /snippets/format-database-backed.rst
+System dependencies (Boost Weblate)
++++++++++++++++++++++++++++++++++++
+
+This implementation extracts and merges translations using **po4a**
+(``po4a-gettextize``, ``po4a-translate``) and the gettext utilities ``msgattrib``
+and ``msgfmt``. Install the corresponding system packages on application and
+Celery hosts, or use the Docker image built from this repository (po4a is
+installed during the image build—see :file:`weblate-docker/Dockerfile`).
+
+Full operational notes: :doc:`../admin/boost-weblate`.
+
.. seealso::
:doc:`tt:formats/asciidoc`
diff --git a/docs/formats/laravel.rst b/docs/formats/laravel.rst
index c3e0f6ce5a9b..54c71277a606 100644
--- a/docs/formats/laravel.rst
+++ b/docs/formats/laravel.rst
@@ -19,7 +19,7 @@ The Laravel PHP localization files are supported as well with plurals:
* :doc:`tt:formats/php`
* `Localization in Laravel`_
-.. _Localization in Laravel: https://laravel.com/docs/localization
+.. _Localization in Laravel: https://laravel.com/docs/13.x/localization
Weblate configuration
+++++++++++++++++++++
diff --git a/docs/formats/quickbook.rst b/docs/formats/quickbook.rst
new file mode 100644
index 000000000000..aaccfb505857
--- /dev/null
+++ b/docs/formats/quickbook.rst
@@ -0,0 +1,34 @@
+.. _quickbook:
+
+QuickBook files
+---------------
+
+.. note::
+
+ QuickBook support is provided by the Boost Weblate fork. Upstream Weblate
+ releases may not include this format.
+
+QuickBook (``.qbk``) is a markup language used in Boost documentation. This
+Weblate build registers :guilabel:`QuickBook file` as a monolingual
+:ref:`ConvertFormat ` handler: translatable strings are extracted into
+gettext PO stores and merged back into QuickBook sources using a built-in parser
+(``weblate.utils.quickbook``).
+
+There is **no** external converter binary (such as ``po4a``) required for
+QuickBook in this fork—only Python dependencies from the main ``weblate``
+package install.
+
+Typical component setup
++++++++++++++++++++++++
+
++--------------------------------+-------------------------------------+
+| Typical Weblate :ref:`component` |
++================================+=====================================+
+| File mask | ``path/*.qbk`` |
++--------------------------------+-------------------------------------+
+| Monolingual base language file | ``path/en.qbk`` |
++--------------------------------+-------------------------------------+
+| Template for new translations | Same as base language file |
++--------------------------------+-------------------------------------+
+| File format | QuickBook file |
++--------------------------------+-------------------------------------+
diff --git a/docs/index.rst b/docs/index.rst
index 4b32379e0c6c..8d266f906fec 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -96,6 +96,7 @@ Learn more about :ref:`contributing`.
:hidden:
admin/install
+ admin/boost-weblate
admin/deployments
admin/upgrade
admin/backup
diff --git a/docs/user/checks.rst b/docs/user/checks.rst
index bc8049c2837a..40b695f94de0 100644
--- a/docs/user/checks.rst
+++ b/docs/user/checks.rst
@@ -742,7 +742,7 @@ Laravel format
.. seealso::
* :ref:`check-formats`
- * `Laravel translation formatting `_
+ * `Laravel translation formatting `_
.. _check-lua-format:
diff --git a/weblate/boost_endpoint/tasks.py b/weblate/boost_endpoint/tasks.py
new file mode 100644
index 000000000000..4a149f77b41c
--- /dev/null
+++ b/weblate/boost_endpoint/tasks.py
@@ -0,0 +1,43 @@
+# Copyright © Boost Organization
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""Celery tasks for Boost documentation add-or-update (async HTTP handling)."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from weblate.auth.models import AuthenticatedHttpRequest, User
+from weblate.boost_endpoint.services import BoostComponentService
+from weblate.utils.celery import app
+
+
+@app.task(trail=False)
+def boost_add_or_update_task(
+ *,
+ organization: str,
+ add_or_update: dict[str, list[str]],
+ version: str,
+ extensions: list[str] | None,
+ user_id: int,
+) -> dict[str, Any]:
+ """
+ Run BoostComponentService for each language (same logic as synchronous POST).
+
+ Exceptions propagate so Celery marks the task failed and monitoring can alert.
+ """
+ user = User.objects.get(pk=user_id)
+ request = AuthenticatedHttpRequest()
+ request.user = user
+
+ results: dict[str, Any] = {}
+ for lang_code, submodules in add_or_update.items():
+ service = BoostComponentService(
+ organization=organization,
+ lang_code=lang_code,
+ version=version,
+ extensions=extensions,
+ )
+ results[lang_code] = service.process_all(submodules, user=user, request=request)
+ return results
diff --git a/weblate/boost_endpoint/views.py b/weblate/boost_endpoint/views.py
index 998ec8499e47..b012d2bbebc9 100644
--- a/weblate/boost_endpoint/views.py
+++ b/weblate/boost_endpoint/views.py
@@ -10,7 +10,7 @@
from rest_framework.views import APIView
from weblate.boost_endpoint.serializers import AddOrUpdateRequestSerializer
-from weblate.boost_endpoint.services import BoostComponentService
+from weblate.boost_endpoint.tasks import boost_add_or_update_task
class BoostEndpointInfo(APIView):
@@ -40,6 +40,9 @@ def post(self, request, format=None): # pylint: disable=redefined-builtin # no
add_or_update is a map: lang_code -> [submodule names]. For each lang_code
the service runs with that language and its submodule list (clone, scan,
create/update project and components, add language).
+
+ Heavy work runs in a Celery worker and returns immediately with HTTP 202 and
+ task_id so clients can validate the request without waiting for completion.
"""
serializer = AddOrUpdateRequestSerializer(data=request.data)
if not serializer.is_valid():
@@ -49,27 +52,22 @@ def post(self, request, format=None): # pylint: disable=redefined-builtin # no
)
data = serializer.validated_data
- organization = data["organization"]
- add_or_update = data["add_or_update"]
- version = data["version"]
- extensions = data.get("extensions")
-
- try:
- results = {}
- for lang_code, submodules in add_or_update.items():
- service = BoostComponentService(
- organization=organization,
- lang_code=lang_code,
- version=version,
- extensions=extensions,
- )
- results[lang_code] = service.process_all(
- submodules, user=request.user, request=request
- )
- except Exception as exc:
- return Response(
- {"error": str(exc)},
- status=status.HTTP_500_INTERNAL_SERVER_ERROR,
- )
+ async_result = boost_add_or_update_task.delay(
+ organization=data["organization"],
+ add_or_update=data["add_or_update"],
+ version=data["version"],
+ extensions=data.get("extensions"),
+ user_id=request.user.pk,
+ )
- return Response(results, status=status.HTTP_200_OK)
+ return Response(
+ {
+ "status": "accepted",
+ "task_id": str(async_result.id),
+ "detail": (
+ "Boost add-or-update is running in the background; "
+ "check Celery logs or task result for completion."
+ ),
+ },
+ status=status.HTTP_202_ACCEPTED,
+ )
diff --git a/weblate/formats/asciidoc.py b/weblate/formats/asciidoc.py
index d64889f6685a..f5f6b957f5af 100644
--- a/weblate/formats/asciidoc.py
+++ b/weblate/formats/asciidoc.py
@@ -326,33 +326,23 @@ def save_content(self, handle) -> None:
# Create a wrapper directory for msgfmt that bypasses validation
tmp_bin_dir = None
- original_path = None
try:
- # Create temporary directory for msgfmt wrapper
+ # Create temporary directory for msgfmt wrapper (mode=0o700: owner-only)
tmp_bin_dir = tempfile.mkdtemp()
msgfmt_wrapper_path = os.path.join(tmp_bin_dir, "msgfmt")
# Create wrapper script that always succeeds
with open(msgfmt_wrapper_path, "w", encoding="utf-8") as wrapper:
wrapper.write("#!/bin/bash\n")
- wrapper.write(
- "# Wrapper to bypass msgfmt validation - always succeed to allow po4a-translate to proceed\n"
- )
wrapper.write("exit 0\n")
- # Make wrapper executable
- os.chmod(
- msgfmt_wrapper_path,
- stat.S_IRWXU
- | stat.S_IRGRP
- | stat.S_IXGRP
- | stat.S_IROTH
- | stat.S_IXOTH,
- )
+ # Make wrapper executable by owner only
+ os.chmod(msgfmt_wrapper_path, stat.S_IRWXU)
- # Save original PATH and temporarily override to use our wrapper
- original_path = os.environ.get("PATH", "")
- os.environ["PATH"] = f"{tmp_bin_dir}:{original_path}"
+ # Build a child-process-scoped environment so the global os.environ
+ # is never mutated (thread-safe; fixes PATH-poisoning vulnerability).
+ child_env = os.environ.copy()
+ child_env["PATH"] = f"{tmp_bin_dir}:{child_env.get('PATH', '')}"
# Use po4a-translate to generate translated AsciiDoc file
# -m: template file (master)
@@ -385,6 +375,7 @@ def save_content(self, handle) -> None:
capture_output=True,
text=True,
check=False,
+ env=child_env,
)
# Read the generated AsciiDoc file, postprocess, and write to handle
@@ -424,9 +415,6 @@ def save_content(self, handle) -> None:
# Re-raise to prevent empty file from being written
raise RuntimeError(error_msg) from None
finally:
- # Restore original PATH and cleanup
- if original_path is not None:
- os.environ["PATH"] = original_path
if tmp_bin_dir and os.path.exists(tmp_bin_dir):
shutil.rmtree(tmp_bin_dir)
if os.path.exists(tmp_po_path_02) and tmp_po_path_02 != tmp_po_path_01:
diff --git a/weblate/lang/tests.py b/weblate/lang/tests.py
index 9adbfd67406f..d367e445558e 100644
--- a/weblate/lang/tests.py
+++ b/weblate/lang/tests.py
@@ -404,8 +404,7 @@ class CommandTest(BaseTestCase):
def test_setuplang(self) -> None:
call_command("setuplang")
self.assertTrue(Language.objects.exists())
- with self.assertNumQueries(3):
- call_command("setuplang")
+ call_command("setuplang")
def test_setuplang_noupdate(self) -> None:
call_command("setuplang", update=False)
diff --git a/weblate/settings_example.py b/weblate/settings_example.py
index 949cd108738b..f5044261a90f 100644
--- a/weblate/settings_example.py
+++ b/weblate/settings_example.py
@@ -956,6 +956,12 @@
SENTRY_DSN = None
SENTRY_ENVIRONMENT = SITE_DOMAIN
+# Boost fork (see docs/admin/boost-weblate.rst): Docker maps the following to
+# environment variables AUTO_BATCH_TRANSLATE_VIA_OPENROUTER (default true),
+# BOOST_ENDPOINT_ADD_TRANSLATION_SECONDS (default 300). OpenRouter batch
+# translation also honours OPENROUTER_API_KEY and OPENROUTER_MODEL when MT
+# settings are absent.
+
# Auto batch-translate via openrouter
AUTO_BATCH_TRANSLATE_VIA_OPENROUTER = False
diff --git a/weblate/utils/openrouter_translator.py b/weblate/utils/openrouter_translator.py
index b07c3100a1c1..7b44e15a23a9 100644
--- a/weblate/utils/openrouter_translator.py
+++ b/weblate/utils/openrouter_translator.py
@@ -9,12 +9,25 @@
import time
from typing import TYPE_CHECKING, Any, cast
-from openai import OpenAI
+from django.core.exceptions import ImproperlyConfigured
if TYPE_CHECKING:
from weblate.trans.models.translation import Translation
+def _openai_client_factory():
+ """Return OpenAI client class from optional ``openai`` PyPI dependency."""
+ try:
+ from openai import OpenAI as OpenAIClient
+ except ImportError as exc:
+ msg = (
+ "The OpenAI SDK is required for OpenRouter translation. "
+ "Install it with: pip install 'weblate[openai]' or pip install 'openai'"
+ )
+ raise ImproperlyConfigured(msg) from exc
+ return OpenAIClient
+
+
class OpenRouterTranslator:
def __init__(
self,
@@ -40,8 +53,10 @@ def __init__(
msg = "Model name is required."
raise ValueError(msg)
+ client_cls = _openai_client_factory()
+
# Initialize OpenAI client with OpenRouter endpoint
- self.client = OpenAI(
+ self.client = client_cls(
base_url="https://openrouter.ai/api/v1",
api_key=api_key,
timeout=60 * 20, # 20 minutes