Skip to content
Open
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
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,14 @@ alibaba = [
"aliyun-python-sdk-core>=2.16.0,<3.0.0"
]
all = [
"weblate[alibaba,amazon,gerrit,gelf,google,ldap,mercurial,openai,postgres,zxcvbn]"
"weblate[alibaba,amazon,argos,gerrit,gelf,google,ldap,mercurial,openai,postgres,zxcvbn]"
]
amazon = [
"boto3>=1.38.0,<2.0"
]
argos = [
"argostranslate"
]
gelf = [
"logging-gelf>=0.0.32,<0.1"
]
Expand Down
88 changes: 88 additions & 0 deletions weblate/machinery/argos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from .base import MachineTranslation

if TYPE_CHECKING:
from weblate.auth.models import User
from weblate.trans.models import Unit

from .base import DownloadTranslations

try:
import argostranslate.package
import argostranslate.translate
except ImportError:
argostranslate = None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This logic should not be needed. When the module cannot be imported, the engine is skipped by Weblate.


logger = logging.getLogger(__name__)


class ArgosTranslation(MachineTranslation):
"""Argos offline machine translation."""

name = "Argos Translate"
settings_form = None
is_available = bool(argostranslate)

def is_supported(self, source_language, target_language):
"""Supported if the language model package is installed in argostranslate."""
if not self.is_available:
return False

src = source_language.split("-")[0].lower()
tgt = target_language.split("-")[0].lower()

installed_languages = argostranslate.translate.get_installed_languages()

src_lang = next(
(lang for lang in installed_languages if lang.code == src), None
)
tgt_lang = next(
(lang for lang in installed_languages if lang.code == tgt), None
)
Comment on lines +39 to +49
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

is_supported() calls argostranslate.translate.get_installed_languages() on every probe and then linearly scans the list twice. Since get_languages() can call is_supported() multiple times per request (language fallback probing), this can add noticeable overhead. Consider caching the installed languages / available pairs (e.g., with a cached_property + explicit cache invalidation) and using a dict lookup instead of repeated linear scans.

Copilot uses AI. Check for mistakes.

if src_lang and tgt_lang:
return src_lang.get_translation(tgt_lang) is not None

return False

def download_translations(
self,
source_language,
target_language,
text: str,
unit: Unit | None,
user: User | None,
threshold: int = 75,
) -> DownloadTranslations:
"""Translate using argostranslate."""
if not self.is_available:
return

src = source_language.split("-")[0].lower()
tgt = target_language.split("-")[0].lower()

try:
translation = argostranslate.translate.translate(text, src, tgt)
if translation:
yield {
"text": translation,
"quality": self.max_score,
"service": self.name,
"source": text,
}
except Exception as e:
logger.debug(
"Argos translation failed for %s to %s: %s",
source_language,
target_language,
e,
)
return
Comment on lines +72 to +88
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

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

ArgosTranslation swallows all exceptions during translation and only logs a debug message, which can silently hide real misconfigurations or runtime failures (users will just see no suggestions). Let the exception propagate so Weblate can wrap/report it, or catch only expected exceptions and raise a MachineTranslationError (and/or call self.report_error(...)) so failures are visible and diagnosable.

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions weblate/machinery/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class WeblateConf(AppConf):
# List of machinery classes
WEBLATE_MACHINERY = (
"weblate.machinery.apertium.ApertiumAPYTranslation",
"weblate.machinery.argos.ArgosTranslation",
"weblate.machinery.aws.AWSTranslation",
"weblate.machinery.alibaba.AlibabaTranslation",
"weblate.machinery.anthropic.AnthropicTranslation",
Expand Down
71 changes: 71 additions & 0 deletions weblate/machinery/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from weblate.machinery.alibaba import AlibabaTranslation
from weblate.machinery.anthropic import AnthropicTranslation
from weblate.machinery.apertium import ApertiumAPYTranslation
from weblate.machinery.argos import ArgosTranslation
from weblate.machinery.aws import AWSTranslation
from weblate.machinery.baidu import BAIDU_API, BaiduTranslation
from weblate.machinery.base import (
Expand Down Expand Up @@ -3407,6 +3408,76 @@ def test_install_valid_form(self) -> None:
)


class ArgosTranslationTest(BaseMachineTranslationTest):
MACHINE_CLS = ArgosTranslation
EXPECTED_LEN = 1

def mock_empty(self) -> None:
self.skipTest("Not tested")

def mock_error(self) -> None:
self.skipTest("Not tested")

def mock_response(self) -> None:
pass

@patch("weblate.machinery.argos.ArgosTranslation.is_available", True)
@patch("weblate.machinery.argos.argostranslate")
def test_translate(self, mock_argostranslate, **kwargs) -> None:
mock_lang_en = MagicMock()
mock_lang_en.code = "en"
mock_lang_cs = MagicMock()
mock_lang_cs.code = "cs"
mock_lang_en.get_translation.return_value = True

mock_argostranslate.translate.get_installed_languages.return_value = [
mock_lang_en,
mock_lang_cs,
]
mock_argostranslate.translate.translate.return_value = "Nazdar"

super().test_translate(**kwargs)

@patch("weblate.machinery.argos.ArgosTranslation.is_available", True)
@patch("weblate.machinery.argos.argostranslate")
def test_batch(self, mock_argostranslate, **kwargs) -> None:
mock_lang_en = MagicMock()
mock_lang_en.code = "en"
mock_lang_cs = MagicMock()
mock_lang_cs.code = "cs"
mock_lang_en.get_translation.return_value = True

mock_argostranslate.translate.get_installed_languages.return_value = [
mock_lang_en,
mock_lang_cs,
]
mock_argostranslate.translate.translate.return_value = "Nazdar"

super().test_batch(**kwargs)

@patch("weblate.machinery.argos.ArgosTranslation.is_available", True)
@patch("weblate.machinery.argos.argostranslate")
def test_support(self, mock_argostranslate, **kwargs) -> None:
mock_lang_en = MagicMock()
mock_lang_en.code = "en"
mock_lang_cs = MagicMock()
mock_lang_cs.code = "cs"
mock_lang_en.get_translation.return_value = True

mock_argostranslate.translate.get_installed_languages.return_value = [
mock_lang_en,
mock_lang_cs,
]

super().test_support(**kwargs)

def test_is_available(self):
machine = self.get_machine()
from weblate.machinery.argos import argostranslate

self.assertEqual(machine.is_available, bool(argostranslate))


class SourceLanguageTranslateTestCase(FixtureTestCase):
LANGUAGE = "de"
SOURCE = "Hello, world!\n"
Expand Down