Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Weblate 2026.7

.. rubric:: New features

* Added :ref:`mt-mistral` machinery integration for Mistral LLM automatic suggestions.

.. rubric:: Improvements

* Management interface access control is now more fine-grained with dedicated site-wide permissions.
Expand Down
2 changes: 2 additions & 0 deletions docs/snippets/addon-parameters-autogenerated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Machine translation engines
- LTEngine
* - ``libretranslate``
- LibreTranslate
* - ``mistral``
- Mistral
* - ``modernmt``
- ModernMT
* - ``mymemory``
Expand Down
50 changes: 50 additions & 0 deletions docs/snippets/machines-autogenerated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,56 @@ You can also specify a custom category to use `custom translator <https://learn.
* `"Authenticating with an access token" section <https://learn.microsoft.com/en-us/azure/ai-services/translator/text-translation/reference/authentication#authenticating-with-an-access-token>`_


.. AUTOGENERATED START: mistral
.. This section is automatically generated by `./manage.py list_machinery`. Do not edit manually.

.. _mt-mistral:

Mistral
-------

.. versionadded:: 2026.7

:Service ID: ``mistral``
:Maximal score: 90
:Advanced features: * :ref:`glossary-mt`
* :ref:`llm-translation-context`
:Configuration: +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``source_language`` | Source language selection | Available choices: |
| | | |
| | | ``auto`` -- Automatic selection |
| | | |
| | | ``source`` -- Component source language |
| | | |
| | | ``secondary`` -- Secondary language defined in project or component |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``base_url`` | Mistral API base URL | Base URL of the Mistral API, if it differs from the Mistral default URL |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``model`` | Mistral model | Available choices: |
| | | |
| | | ``auto`` -- Automatic selection |
| | | |
| | | ``mistral-small-latest`` -- Mistral Small |
| | | |
| | | ``mistral-medium-latest`` -- Mistral Medium |
| | | |
| | | ``mistral-large-latest`` -- Mistral Large |
| | | |
| | | ``custom`` -- Custom model |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``persona`` | Translator persona | Describe the persona of translator to improve the accuracy of the translation. For example: “You are a squirrel breeder.” |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``style`` | Translator style | Describe the style of translation. For example: “Use informal language.” |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``language_instructions`` | Language-specific instructions | JSON object mapping existing target language codes to extra instructions, up to 1000 characters each. |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``key`` | API key | |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``custom_model`` | Custom model name | Only needed when model is set to 'Custom model' |
+---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+


.. AUTOGENERATED END: mistral
.. AUTOGENERATED START: modernmt
.. This section is automatically generated by `./manage.py list_machinery`. Do not edit manually.

Expand Down
1 change: 1 addition & 0 deletions weblate/machinery/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"weblate.machinery.youdao.YoudaoTranslation",
"weblate.machinery.systran.SystranTranslation",
"weblate.machinery.openai.OpenAITranslation",
"weblate.machinery.mistral.MistralTranslation",
"weblate.machinery.ollama.OllamaTranslation",
"weblate.machinery.openai.AzureOpenAITranslation",
"weblate.machinery.weblatetm.WeblateTranslation",
Expand Down
52 changes: 52 additions & 0 deletions weblate/machinery/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@
),
required=False,
)
model = forms.ChoiceField(

Check failure on line 556 in weblate/machinery/forms.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible types in assignment (expression has type "ChoiceField", base class "LLMBasicMachineryForm" defined the type as "CharField")
label=pgettext_lazy(
"Automatic suggestion service configuration",
"OpenAI model",
Expand Down Expand Up @@ -585,6 +585,58 @@
super().clean()


class MistralMachineryForm(BaseOpenAIMachineryForm):
# Ordering choices here defines priority for automatic selection
MODEL_CHOICES = (
("auto", pgettext_lazy("Mistral model selection", "Automatic selection")),
("mistral-small-latest", "Mistral Small"),
("mistral-medium-latest", "Mistral Medium"),
("mistral-large-latest", "Mistral Large"),
("custom", pgettext_lazy("Mistral model selection", "Custom model")),
)
base_url = WeblateServiceURLField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Mistral API base URL",
),
widget=forms.TextInput,
help_text=gettext_lazy(
"Base URL of the Mistral API, if it differs from the Mistral default URL"
),
required=False,
)
model = forms.ChoiceField(

Check failure on line 608 in weblate/machinery/forms.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible types in assignment (expression has type "ChoiceField", base class "LLMBasicMachineryForm" defined the type as "CharField")
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Mistral model",
),
initial="auto",
choices=MODEL_CHOICES,
)
custom_model = forms.CharField(
label=pgettext_lazy(
"Mistral model selection",
"Custom model name",
),
help_text=gettext_lazy("Only needed when model is set to 'Custom model'"),
required=False,
)

def clean(self) -> None:
"""Validate custom_model and model fields."""
has_custom_model = bool(self.cleaned_data.get("custom_model"))
model = self.cleaned_data.get("model")
if model == "custom" and not has_custom_model:
raise ValidationError(
{"custom_model": gettext("Missing custom model name.")}
)
if model != "custom" and has_custom_model:
raise ValidationError(
{"model": gettext("Choose custom model here to enable it.")}
)
super().clean()


class AzureOpenAIMachineryForm(BaseOpenAIMachineryForm):
azure_endpoint = WeblateServiceURLField(
label=pgettext_lazy(
Expand Down Expand Up @@ -653,7 +705,7 @@
initial="https://api.anthropic.com",
required=False,
)
model = forms.ChoiceField(

Check failure on line 708 in weblate/machinery/forms.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible types in assignment (expression has type "ChoiceField", base class "LLMBasicMachineryForm" defined the type as "CharField")
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Anthropic model",
Expand Down
28 changes: 28 additions & 0 deletions weblate/machinery/mistral.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

from typing import ClassVar

from .forms import MistralMachineryForm
from .openai import OpenAITranslation


class MistralTranslation(OpenAITranslation):
"""
Mistral machine translation integration.

Configurable machine translation interface that uses Mistral language
models through its OpenAI-compatible API.
"""

name = "Mistral"
trusted_error_hosts: ClassVar[set[str]] = {"api.mistral.ai"}

settings_form = MistralMachineryForm

Check failure on line 24 in weblate/machinery/mistral.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible types in assignment (expression has type "type[MistralMachineryForm]", base class "OpenAITranslation" defined the type as "type[OpenAIMachineryForm]")
version_added = "2026.7"

def get_runtime_base_url(self) -> str:
return self.settings.get("base_url") or "https://api.mistral.ai/v1"
127 changes: 120 additions & 7 deletions weblate/machinery/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
PROMPT,
)
from weblate.machinery.microsoft import MicrosoftCognitiveTranslation
from weblate.machinery.mistral import MistralTranslation
from weblate.machinery.modernmt import ModernMTTranslation
from weblate.machinery.mymemory import MyMemoryTranslation
from weblate.machinery.netease import NETEASE_API_ROOT, NeteaseSightTranslation
Expand Down Expand Up @@ -1049,8 +1050,8 @@
machine.validate_settings()
self.assertEqual(len(responses.calls), 2)
call_2 = responses.calls[1]
self.assertIn("langpair", call_2.request.params)

Check failure on line 1053 in weblate/machinery/tests.py

View workflow job for this annotation

GitHub Actions / mypy

"PreparedRequest" has no attribute "params"
self.assertEqual("eng|spa", call_2.request.params["langpair"])

Check failure on line 1054 in weblate/machinery/tests.py

View workflow job for this annotation

GitHub Actions / mypy

"PreparedRequest" has no attribute "params"

@responses.activate
def test_translations_cache(self) -> None:
Expand Down Expand Up @@ -1850,7 +1851,7 @@
return 200, {}, json.dumps(SYSTRAN_LANGUAGE_JSON)

def translate_callback(request: PreparedRequest):
query = parse_qs(urlparse(request.url).query)

Check failure on line 1854 in weblate/machinery/tests.py

View workflow job for this annotation

GitHub Actions / mypy

Value of type variable "AnyStr" of "parse_qs" cannot be "Sequence[object]"
self.assertEqual(request.headers["Authorization"], "Key key")
self.assertNotIn("key", query)
self.assertEqual(query["source"], ["en"])
Expand Down Expand Up @@ -2042,7 +2043,7 @@

def translate_request_callback(request: PreparedRequest):
"""Check 'glossaries' included in request params."""
self.assertIn("glossaries", request.params)

Check failure on line 2046 in weblate/machinery/tests.py

View workflow job for this annotation

GitHub Actions / mypy

"PreparedRequest" has no attribute "params"
return (200, {}, json.dumps(MODERNMT_RESPONSE))

machine = self.MACHINE_CLS(self.CONFIGURATION)
Expand Down Expand Up @@ -2072,7 +2073,7 @@

def delete_glossary_callback(request: PreparedRequest, expected_id: int):
"""Check that the stale glossary is being deleted."""
self.assertTrue(request.url.endswith(f"memories/{expected_id}"))

Check failure on line 2076 in weblate/machinery/tests.py

View workflow job for this annotation

GitHub Actions / mypy

Item "None" of "str | None" has no attribute "endswith"
return (200, {}, "{}")

responses.add_callback(
Expand Down Expand Up @@ -2182,7 +2183,7 @@

def request_callback(request: PreparedRequest):
"""Check 'context_vector' included in request body."""
self.assertIn("context_vector", request.params)

Check failure on line 2186 in weblate/machinery/tests.py

View workflow job for this annotation

GitHub Actions / mypy

"PreparedRequest" has no attribute "params"
return (200, {}, json.dumps(MODERNMT_RESPONSE))

responses.add_callback(
Expand Down Expand Up @@ -3238,10 +3239,10 @@
"object": "list",
"data": [
{
"id": "gpt-5-nano",
"id": OpenAITranslationTest.TRACE_MODEL,
"object": "model",
"created": 1686935002,
"owned_by": "openai",
"owned_by": "test",
}
],
},
Expand Down Expand Up @@ -5183,10 +5184,10 @@
"object": "list",
"data": [
{
"id": "gpt-5-nano",
"id": self.TRACE_MODEL,
"object": "model",
"created": 1686935002,
"owned_by": "openai",
"owned_by": "test",
}
],
},
Expand Down Expand Up @@ -5274,10 +5275,10 @@
"object": "list",
"data": [
{
"id": "gpt-5-nano",
"id": self.TRACE_MODEL,
"object": "model",
"created": 1686935002,
"owned_by": "openai",
"owned_by": "test",
}
],
},
Expand All @@ -5294,12 +5295,123 @@
},
),
):
self.assertEqual(machine.get_model(), "gpt-5-nano")
self.assertEqual(machine.get_model(), self.TRACE_MODEL)

mocked_getaddrinfo.assert_not_called()
self.assertEqual(len(responses.calls), 1)


class MistralTranslationTest(OpenAITranslationTest):
MACHINE_CLS: type[BatchMachineTranslation] = MistralTranslation
CONFIGURATION: ClassVar[SettingsDict] = {
"key": "x",
"model": "auto",
"persona": "",
"style": "",
}
TRACE_MODEL: ClassVar[str] = "mistral-small-latest"

@staticmethod
def mock_models() -> None:
responses.add(
responses.GET,
"https://api.mistral.ai/v1/models",
json={
"object": "list",
"data": [
{
"id": "mistral-small-latest",
"object": "model",
"created": 1686935002,
"owned_by": "mistral",
}
],
},
)

def mock_response(self, content: str = '["Ahoj světe"]') -> None:
self.mock_models()
responses.add(
responses.POST,
"https://api.mistral.ai/v1/chat/completions",
json={
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "mistral-small-latest",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": content,
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
},
},
)


class MistralCustomTranslationTest(OpenAICustomTranslationTest):
MACHINE_CLS: type[BatchMachineTranslation] = MistralTranslation
CONFIGURATION: ClassVar[SettingsDict] = {
"key": "x",
"model": "auto",
"persona": "",
"style": "",
"base_url": "https://custom.example.com/",
}
TRACE_MODEL: ClassVar[str] = "mistral-small-latest"

def mock_response(self, content: str = '["Ahoj světe"]') -> None:
responses.add(
responses.GET,
"https://custom.example.com/models",
json={
"object": "list",
"data": [
{
"id": "mistral-small-latest",
"object": "model",
"created": 1686935002,
"owned_by": "mistral",
}
],
},
)
responses.add(
responses.POST,
"https://custom.example.com/chat/completions",
json={
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "mistral-small-latest",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": content,
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
},
},
)


class AzureOpenAITranslationTest(OpenAITranslationTest):
MACHINE_CLS: type[BatchMachineTranslation] = AzureOpenAITranslation
CONFIGURATION: ClassVar[SettingsDict] = {
Expand Down Expand Up @@ -6327,6 +6439,7 @@
LibreTranslateTranslation,
LTEngineTranslation,
MicrosoftCognitiveTranslation,
MistralTranslation,
ModernMTTranslation,
MyMemoryTranslation,
NeteaseSightTranslation,
Expand Down
Loading