Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LibreTranslate machine translator, documentation and its test cases. #753

Closed
wants to merge 8 commits into from
19 changes: 19 additions & 0 deletions docs/how-to/integrations/machine-translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ WAGTAILLOCALIZE_MACHINE_TRANSLATOR = {
}
```

## LibreTranslate

Website: [https://libretranslate.com/](https://libretranslate.com/)

!!! note

You will need a subscription to get an API key. Alternatively, you can host your own instance. See more details at [https://github.com/LibreTranslate/LibreTranslate](https://github.com/LibreTranslate/LibreTranslate).

```python
WAGTAILLOCALIZE_MACHINE_TRANSLATOR = {
"CLASS": "wagtail_localize.machine_translators.libretranslate.LibreTranslator",
"OPTIONS": {
"LIBRETRANSLATE_URL": "https://libretranslate.org", # or your self-hosted instance URL
# For self-hosted instances without API key setup, use a random string as the API key.
"API_KEY": "<Your LibreTranslate api key here>",
},
}
```

## Dummy

The dummy translator exists primarily for testing Wagtail Localize and it only reverses the strings that are passed to
Expand Down
50 changes: 50 additions & 0 deletions wagtail_localize/machine_translators/libretranslate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json

import requests

from wagtail_localize.machine_translators.base import BaseMachineTranslator
from wagtail_localize.strings import StringValue


class LibreTranslator(BaseMachineTranslator):
"""
A machine translator that uses the LibreTranslate API.
drivard marked this conversation as resolved.
Show resolved Hide resolved

API Documentation:
https://libretranslate.com/docs/
"""

display_name = "LibreTranslate"

def get_api_endpoint(self):
return self.options["LIBRETRANSLATE_URL"]

def language_code(self, code):
return code.split("-")[0]

def translate(self, source_locale, target_locale, strings):
translations = [item.data for item in list(strings)]
response = requests.post(
self.get_api_endpoint() + "/translate",
data=json.dumps(
{
"q": translations,
"source": self.language_code(source_locale.language_code),
"target": self.language_code(target_locale.language_code),
"api_key": self.options["API_KEY"],
}
),
headers={"Content-Type": "application/json"},
timeout=10,
)
response.raise_for_status()

return {
string: StringValue(translation)
for string, translation in zip(strings, response.json()["translatedText"])
}

def can_translate(self, source_locale, target_locale):
return self.language_code(source_locale.language_code) != self.language_code(
target_locale.language_code
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from unittest import mock

from django.test import TestCase, override_settings
from wagtail.models import Locale

from wagtail_localize.machine_translators import get_machine_translator
from wagtail_localize.machine_translators.libretranslate import LibreTranslator
from wagtail_localize.strings import StringValue


LIBRETRANSLATE_SETTINGS_ENDPOINT = {
"CLASS": "wagtail_localize.machine_translators.libretranslate.LibreTranslator",
"OPTIONS": {
"LIBRETRANSLATE_URL": "https://libretranslate.org",
"API_KEY": "test-api-key",
},
}


class TestLibreTranslator(TestCase):
@override_settings(
WAGTAILLOCALIZE_MACHINE_TRANSLATOR=LIBRETRANSLATE_SETTINGS_ENDPOINT
)
def setUp(self):
self.english_locale = Locale.objects.get()
self.french_locale = Locale.objects.create(language_code="fr-fr")
drivard marked this conversation as resolved.
Show resolved Hide resolved
self.translator = get_machine_translator()

def test_api_endpoint(self):
self.assertIsInstance(self.translator, LibreTranslator)
api_endpoint = self.translator.get_api_endpoint()
self.assertEqual(api_endpoint, "https://libretranslate.org")

@mock.patch(
drivard marked this conversation as resolved.
Show resolved Hide resolved
"wagtail_localize.machine_translators.libretranslate.LibreTranslator.translate",
return_value={
StringValue("Hello world!"): StringValue("Bonjour le monde!"),
StringValue("This is a sentence. This is another sentence."): StringValue(
"Ceci est une phrase. Ceci est une autre phrase."
),
},
)
def test_translate_text(self, mock_translate):
self.assertIsInstance(self.translator, LibreTranslator)

translations = self.translator.translate(
self.english_locale,
self.french_locale,
{
StringValue("Hello world!"),
StringValue("This is a sentence. This is another sentence."),
},
)

self.assertEqual(
translations,
{
StringValue("Hello world!"): StringValue("Bonjour le monde!"),
StringValue(
"This is a sentence. This is another sentence."
): StringValue("Ceci est une phrase. Ceci est une autre phrase."),
},
)

@mock.patch(
"wagtail_localize.machine_translators.libretranslate.LibreTranslator.translate",
return_value={
StringValue('<a id="a1">Hello !</a>. <b>This is a test</b>.'): StringValue(
"""<a id="a1">Bonjour !</a>. <b>C'est un test</b>."""
),
},
)
def test_translate_html(self, mock_translate):
self.assertIsInstance(self.translator, LibreTranslator)

string, attrs = StringValue.from_source_html(
'<a href="https://en.wikipedia.org/wiki/World">Hello !</a>. <b>This is a test</b>.'
)

translations = self.translator.translate(
self.english_locale, self.french_locale, [string]
)

self.assertEqual(
translations[string].render_html(attrs),
"""<a href="https://en.wikipedia.org/wiki/World">Bonjour !</a>. <b>C'est un test</b>.""",
)

def test_can_translate(self):
self.assertIsInstance(self.translator, LibreTranslator)

french_locale = Locale.objects.create(language_code="fr")

self.assertTrue(
self.translator.can_translate(self.english_locale, self.french_locale)
)
self.assertTrue(
self.translator.can_translate(self.english_locale, french_locale)
)

# Can't translate the same language
self.assertFalse(
self.translator.can_translate(self.english_locale, self.english_locale)
)

# Can't translate two variants of the same language
self.assertFalse(
self.translator.can_translate(self.french_locale, french_locale)
)